Redis分布式锁
1022 字
5 分钟
Redis分布式锁
分布式锁
分布式锁是满足分布式系统或集群模式下多进程可见并且互斥的锁。
基本要求:
- 互斥性
- 多进程可见
- 安全性
- 高可用
- 高性能
- 防止死锁
常见实现:
| 实现方式 | 核心机制 | 特点 |
|---|---|---|
| MySQL | 唯一索引、行锁或排他锁 | 实现方便,性能通常低于Redis |
| Redis | SET NX EX | 性能高,需要处理过期和误删问题 |
| ZooKeeper | 临时节点、顺序节点 | 一致性较强,连接断开后临时节点自动删除 |
基于Redis实现分布式锁
ILock.lua
public interface ILock {
/** * 尝试获取锁 * @param timeoutSec 锁持有的超时时间,过期后自动释放 * @return true代表获取锁成功,false代表获取锁失败 */ boolean tryLock(long timeoutSec);
/** * 释放锁 */ void unlock();}SimpleRedisLock.java
public class SimpleRedisLock implements ILock {
private String name; private StringRedisTemplate stringRedisTemplate;
public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate){ this.name = name; this.stringRedisTemplate = stringRedisTemplate; } private static final String KEY_PREFIX="lock";
@Override public boolean tryLock(long timeoutSec) { // 获取线程标识 long threadId = Thread.currentThread().getId(); // 获取锁 Boolean success = stringRedisTemplate.opsForValue(). setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS); return Boolean.TRUE.equals(success); }
@Override public void unlock() { // 释放锁 stringRedisTemplate.delete(KEY_PREFIX + name); }}setIfAbsent同时传入过期时间时,底层相当于一个原子的Redis命令:
SET lock thread1 NX EX 10- NX保证只有Key不存在时才能获取锁
- EX设置过期时间,避免服务宕机后产生死锁
VoucherOrderServiceImpl.java
Long userId = UserHolder.getUser().getId();// 创建锁对象SimpleRedisLock lock = new SimpleRedisLock("order" + userId, stringRedisTemplate);// 获取锁boolean isLock = lock.tryLock(1200);// 判断是否获取锁成功if(!isLock){ // 获取锁失败,返回错误或重试 return Result.fail("不允许重复下单");}try{ // 获取代理对象(事物) IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy(); return proxy.createVoucherOrder(voucherId);} finally { // 释放锁 lock.unlock();}Redis分布式锁误删问题
线程1获取锁成功线程1执行时间过长,锁自动过期线程2获取到同一把锁线程1执行完成,直接执行DEL线程1错误地删除了线程2的锁线程3也可以获取锁并进入业务锁中需要保存锁持有者的唯一标识:
UUID + 线程IDUUID用于区分不同JVM,线程ID用于区分同一个JVM中的不同线程。
改进Redis的分布锁
private static final String ID_PREFIX= UUID.randomUUID().toString(true)+"-";
// 获取线程标识String threadId = ID_PREFIX+Thread.currentThread().getId();// 获取锁Boolean success = stringRedisTemplate.opsForValue(). setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);UUID.randomUUID().toString(true)是Hutool UUID的写法。如果使用JDK的java.util.UUID,应写为:
UUID.randomUUID().toString()SimpleRedisLock.java
@Overridepublic void unlock() { // 获取线程标识 String threadId= ID_PREFIX+Thread.currentThread().getId(); // 获取锁中的标识 String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name); // 判断标识是否一致 if(threadId.equals(id)){ // 释放锁 stringRedisTemplate.delete(KEY_PREFIX + name); }}Redis分布式锁原子性问题
上面的GET和DEL是两个独立操作,仍然可能出现:
线程1执行GET,确认锁属于自己线程1发生阻塞锁到期自动删除线程2获取锁线程1恢复执行DEL线程1删除了线程2的锁因此“判断锁标识”和“删除锁”必须作为一个原子操作执行。
Redis的Lua脚本
Redis可以在一个Lua脚本中编写多条Redis命令。Redis会将整个脚本作为一个整体执行,脚本执行期间不会插入其他命令。
基本语法:
redis.call('命令名称', 'key', '其它参数')示例:
redis.call('set', 'name', 'jack')执行多条命令:
redis.call('set', 'name', 'jack')local name = redis.call('get', 'name')return nameEVAL命令
EVAL script numkeys key [key ...] arg [arg ...]固定参数:
EVAL "return redis.call('set', 'name', 'jack')" 0动态参数:
EVAL "return redis.call('set', KEYS[1], ARGV[1])" 1 name Rose参数对应关系:
1 脚本需要一个Keyname 放入KEYS[1]Rose 放入ARGV[1]Lua数组从1开始,因此使用KEYS[1]和ARGV[1]。
使用Lua脚本释放锁
unLock.lua
--- 比较线程标识与锁中的标识是否一致if(redis.call('get',KEYS[1]) == ARGV[1]) then --- 释放锁 del key return redis.call('del',KEYS[1])endreturn 0脚本将读取标识、比较标识和删除锁放在同一次执行中,保证操作的原子性。
Spring调用Lua脚本
SimpleRedisLock.java
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;static { UNLOCK_SCRIPT = new DefaultRedisScript<>(); UNLOCK_SCRIPT.setLocation(new ClassPathResource("unLock.lua")); UNLOCK_SCRIPT.setResultType(Long.class);}
@Overridepublic void unlock() { // 调用lua脚本 stringRedisTemplate.execute( UNLOCK_SCRIPT, Collections.singletonList(KEY_PREFIX+name), ID_PREFIX+Thread.currentThread().getId() );}













