一、分布式锁实现原理
🎄 分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁
二、不同的分布式锁实现方案
🎄 分布式锁的核心是实现多进程之间锁的互斥,而满足这一点的方式有很多,常见的有三种:
三、Redis 的 setnx 实现互斥锁
🎄锁获取了,还没有来得及设置过期时间服务器就宕机了
🎄保证 setnx(获取锁)和 expire 设置过期时间两个操作是原子性的
四、基于 Redis 实现分布式锁初级版
🎄 需求:定义一个类,实现下面的接口,利用 Redis 实现分布式锁功能
public interface LockInter { /** * 尝试获取锁 * * @param ttlSecond 锁的过期时间 * @return true: 成功获取锁; false: 获取锁失败 */ boolean tryLock(long ttlSecond); /** * 释放锁 */ void unlock(); }
public class LockImplV1 implements LockInter { private String name; // 和业务相关的锁的名字 private StringRedisTemplate stringRedisTemplate; private static final String LOCK_KEY_PREFIX = "lock:"; public LockImplV1(String name, StringRedisTemplate stringRedisTemplate) { this.name = name; this.stringRedisTemplate = stringRedisTemplate; } @Override public boolean tryLock(long ttlSecond) { String key = LOCK_KEY_PREFIX + name; // value 里面放当前线程的唯一标识(线程 ID) String val = Thread.currentThread().getId() + ""; Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(key, val, ttlSecond, TimeUnit.SECONDS); // Boolean -- boolean 会自动拆箱 // 当 success 为 null 的时候会抛异常 return Boolean.TRUE.equals(success); } @Override public void unlock() { stringRedisTemplate.delete(LOCK_KEY_PREFIX + name); } }
五、误删锁问题(业务阻塞导致)
需求:修改之前的分布式锁实现,满足:
- 在获取锁时存入线程标识(可以用 UUID 表示)
- 在释放锁时先获取锁中的线程标识,判断是否与当前线程标识一致
① 如果一致则释放锁
② 如果不一致则不释放锁
要用 UUID,避免线程 ID 重复
public class LockImplV2 implements LockInter { private String name; // 和业务相关的锁的名字 private StringRedisTemplate stringRedisTemplate; private static final String LOCK_KEY_PREFIX = "lock:"; private static final String UNIQUE_PREFIX = UUID.randomUUID().toString(true); public LockImplV2(String name, StringRedisTemplate stringRedisTemplate) { this.name = name; this.stringRedisTemplate = stringRedisTemplate; } @Override public boolean tryLock(long ttlSecond) { String key = LOCK_KEY_PREFIX + name; // value 里面放当前线程的唯一标识(线程 ID) String val = UNIQUE_PREFIX + Thread.currentThread().getId(); Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(key, val, ttlSecond, TimeUnit.SECONDS); // Boolean -- boolean 会自动拆箱 // 当 success 为 null 的时候会抛异常 return Boolean.TRUE.equals(success); } @Override public void unlock() { String k = LOCK_KEY_PREFIX + name; String cacheVal = stringRedisTemplate.opsForValue().get(k); String curVal = UNIQUE_PREFIX + Thread.currentThread().getId(); if (curVal.equals(cacheVal)) { stringRedisTemplate.delete(k); } } }
六、误删锁(Redis 命令原子性导致)
解决方案:Lua 脚本
(1) Lua 脚本
📖 Redis 提供了 Lua 脚本功能,在一个脚本中编写多条 Redis 命令,确保多条命令执行时的原子性
📖 Lua 是一种编程语言 https://www.runoob.com/lua/lua-tutorial.html
(2) Redis 编写和执行 Lua 脚本
参数有两种:key 类型参数,其他参数
📖 如果脚本中的 key(gender)、value(handsomeBoy)不想写死,可以作为参数传递
📖 key 类型参数会放入 KEYS 数组
📖 其它参数会放入 ARGV 数组,在脚本中可以从 KEYS 和 ARGV 数组获取这些参数
Lua 语言中下标从 1 开始
(3) 复杂逻辑的 Lua 脚本(业务相关)
📖 获取锁(Redis 缓存)中的线程标识 cacheVal
📖 判断是否与当前线程标识一致 curVal
📖 如果一致则释放锁(del)
📖 如果不一致则什么都不做
上述操作要通过 Lua 脚本执行,保证多条 Redis 命令的原子性(防止误删锁)
--- 当前线程的线程标识 local curVal = ARGV[1] --- 要删除的锁的 key local lockKey = KEYS[1] if(cacheVal == curVal) then return redis.call('DEL', KEYS[1]) end return 0
(4) RedisTemplate 执行 Lua 脚本
Lua 脚本可写在 Java 的类路径下的资源文件夹中
public class LockImplV3 implements LockInter { private String name; // 和业务相关的锁的名字 private StringRedisTemplate stringRedisTemplate; private static final String LOCK_KEY_PREFIX = "lock:"; private static final String UNIQUE_PREFIX = UUID.randomUUID().toString(true); private static final DefaultRedisScript<Long> UNLOCK_LUA_SCRIPT; static { // 初始化 UNLOCK_LUA_SCRIPT UNLOCK_LUA_SCRIPT = new DefaultRedisScript<>(); UNLOCK_LUA_SCRIPT.setLocation(new ClassPathResource("unlock.lua")); UNLOCK_LUA_SCRIPT.setResultType(Long.class); } public LockImplV3(String name, StringRedisTemplate stringRedisTemplate) { this.name = name; this.stringRedisTemplate = stringRedisTemplate; } @Override public boolean tryLock(long ttlSecond) { String key = LOCK_KEY_PREFIX + name; // value 里面放当前线程的唯一标识(线程 ID) String val = UNIQUE_PREFIX + Thread.currentThread().getId(); Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(key, val, ttlSecond, TimeUnit.SECONDS); // Boolean -- boolean 会自动拆箱 // 当 success 为 null 的时候会抛异常 return Boolean.TRUE.equals(success); } @Override public void unlock() { stringRedisTemplate.execute( UNLOCK_LUA_SCRIPT, Collections.singletonList(LOCK_KEY_PREFIX + name), UNIQUE_PREFIX + Thread.currentThread().getId()); } }