🌹 以下分享 Redis 分布式锁,如有问题请指教。
🌹🌹 如你对技术也感兴趣,欢迎交流。
🌹🌹🌹 如有对阁下帮助,请👍点赞💖收藏🐱🏍分享😀
Redis 除了做缓存,其他基于Redis的用法。(答案:数据共享,分布式session/分布式锁/全局ID/计算器、点赞/位统计/购物车/轻量级消息队列(list/stream)/抽奖/签到打卡/交差并算法/热点排行榜) |
Redis 做分布式锁的时候需要注意的问题 |
是否使用setnx 命令实现分布式锁?合适吗?如何考虑分布式锁可重入问题 |
Redis 单点部署会有什么问题 |
Redis 集群下,比如主从模式,CAP方面有什么问题 |
简单介绍一下RedLock,Redisson |
Redis 分布式锁如何续期?看门狗(watch Dog)了解吗 |
Redis 集群保证(AP高可用),Redis 单机(C: 一致性)
锁的种类
单机锁,同一JVM虚拟机 | synchronized/Lock |
分布式锁,多个不同的JVM虚拟机 |
分布式锁满足条件
独占性 | 高可用 | 防死锁 | 不乱抢 | 可重入 |
OnlyOne ,任何时刻只能有仅有一个线程持有 | 若Redis集群环境下,不能因为某个节点下线而出现获取锁和释放锁失败的情况 高并发情况下,依然性能俱佳 | 杜绝死锁,必须有超时控制机制或撤销操作,有个兜底跳出方案。 | 防止张冠李戴,只能释放自己的锁 | 同一节点的同一线程如果获得锁之后,它可以再次获得这个锁。 |
服务结构
公共代码
// 扣减库存
private String excuteSale(String message) {
// 查询库存
String sales = redisTemplate.opsForValue().get(KEY_);
//判断库存是否足够
Integer restSale = StrUtil.isEmpty(sales) ? null : Integer.parseInt(sales);
//扣减数量
if (Objects.nonNull(restSale) && restSale > 0) {
redisTemplate.opsForValue().set(KEY_, String.valueOf(--restSale));
message = String.format("卖出商品,库存剩余 %s,服务端口%s", restSale, port);
} else {
message = String.format("商品已售罄,服务端口%s", port);
}
return message;
}
案列V1.0
private final StringRedisTemplate redisTemplate;
private Lock lock = new ReentrantLock();
private final static String KEY_ = "sale:001";
public String sale() {
String message = "";
lock.lock();
try {
message = excuteSale(message);
} finally {
lock.unlock();
}
System.out.println(message);
return message;
}
Jmeter压测
问题
8888和9999 同时卖出,出现超卖
原因
在单机环境下,可以使用synchronized或Lock来实现 ;
在分布式架构中,因竞争的线程在不同的节点上,需要一个让所有进程都能访问的锁来实现阻塞(比如redis 或者zookeeper)
案列V2.0
引入Redis 分布式锁
// v2.0
public String sale() {
String message = "";
String redis_key = "rdl_sale_lock";
String uuidVal = IdUtil.simpleUUID() + "_" + Thread.currentThread().getId();
Boolean flag = redisTemplate.opsForValue().setIfAbsent(redis_key, uuidVal);
// flag=false ,抢不到的线程继续重试
if (!flag) {
// 暂停 20毫秒,进行递归重试
try {
TimeUnit.MILLISECONDS.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
sale();
} else {
// 抢到锁的线程,正常业务执行,扣减库存
try {
message = excuteSale(message);
} finally {
redisTemplate.delete(redis_key);
}
}
System.out.println(message);
return message;
}
功能测试通过
潜在问题
递归使用易导致StackOverflowError问题,使用while 替换if;
解决方法
使用自旋代替递归
自旋的案例
public String sale() {
String message = "";
String redis_key = "rdl_sale_lock";
String uuidVal = IdUtil.simpleUUID() + "_" + Thread.currentThread().getId();
//递归,高并发下容易出错,用自旋替代递归方法重试调用: 用while来替代
while (!redisTemplate.opsForValue().setIfAbsent(redis_key, uuidVal)) {
// 暂停 20毫秒,进行递归重试
try {
TimeUnit.MILLISECONDS.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 抢到锁的线程,正常业务执行,扣减库存
try {
message = excuteSale(message);
} finally {
redisTemplate.delete(redis_key);
}
System.out.println(message);
return message;
}
案例V3.0
案例问题
锁未加过期时间
当服务突然中断时,锁一直不能被占用未被删除,其他服务无法使用
加入过期时间(不完美)
潜在存在的问题
高并发下,设置key和过期时间分开,会造成未加过期时间同等问题
解决方法
必须要合并成一行具备原子性
//递归,高并发下容易出错,用自旋替代递归方法重试调用: 用while来替代; 加入过期时间
while (!redisTemplate.opsForValue().setIfAbsent(redis_key, uuidVal,10L,TimeUnit.SECONDS)) {
// 暂停 20毫秒,进行递归重试
try {
TimeUnit.MILLISECONDS.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
深层问题01
误删分布式锁
A 先获取锁成功执行业务,正常情况下,锁过期时间30秒,业务执行完成,A释放锁;异常情况,A在执行业务过程中出现卡顿等情况,30秒未能完成业务执行;锁自动被释放;B获取到锁,开始执行业务,这时候A执行业务完成将B获取的锁释放,B业务完成后释放锁时,不能获取到自己的锁。
解决方法
//判断加锁与解锁是不是同一个客户端自己只能删除自己的锁,不误删他人的
if (redisTemplate.opsForValue().get(redis_key) == uuidVal) {
redisTemplate.delete(redis_key);
}
深层问题02
问题
判断跟删除操作非原子操作,可能出现业务影响;用Lua 脚本保证原子性
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
解决方法
// 抢到锁的线程,正常业务执行,扣减库存
try {
message = excuteSale(message);
} finally {
//修改为Lua脚本
String luaScript="if redis.call('get',KEYS[1])==ARGV[1] then " +
"return redis.call('del',KEYS[1]) " +
"else " +
"return 0 " +
"end";
redisTemplate.execute(new DefaultRedisScript<>(luaScript,Long.class), ListUtil.list(false,redis_key),uuidVal);
}
可重入性问题
一个线程中的多个流程可以获取同一把锁,持有这个同步锁可以再次进入。
自己可以获取自己的内部锁
setnx 不满足可重入,HSET 可实现可重入

将加锁和解锁的代码使用Lua 脚本封装成对应的lock和unlock方法,使其符合AQS规范
加锁(lock)Lua
# 加锁
# v 1.0
if redis.call('EXISTS','key')==0 then
redis.call('HSET','key','uuid:threadid',1)
redis.call('EXPIRE','key','30')
return 1
elseif redis.call('HEXISTS','key','uuid:threadid')==1 then
redis.call('HINCRBY','key','uuid:threadid',1)
redis.call('EXPIRE','key','30')
return 1
else
return 0
end
# v2.0
if redis.call('EXISTS','key')==0 or redis.call('HEXISTS','key','uuid:threadid')==1 then
redis.call('HINCRBY','key','uuid:threadid',1)
redis.call('EXPIRE','key','30')
return 1
else
return 0
end
# v3.0 换参数
EVAL "if redis.call('EXISTS',KEYS[1]) == 0 or redis.call('HEXISTS',KEYS[1],ARGV[1]) == 1 then redis.call('HINCRBY',KEYS[1],ARGV[1],1) redis.call('EXPIRE',KEYS[1],ARGV[2]) return 1 else return 0 end" 1 rdl_sale_lock 999:001 30
解锁(unlock)Lua
# 解锁
# v1.0
if redis.call('HEXISTS','key','uuid:threadid') == 0 then
return nil
elseif redis.call('HINCRBY','key','uuid:threadid',-1) == 0 then
return redis.call('DEL','key')
else
return 0
end
# v2.0
if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then
return nil
elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then
return redis.call('DEL',KEYS[1])
else
return 0
end
# v3.0
if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then return nil elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then return redis.call('DEL',KEYS[1]) else return 0 end
EVAL "if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then return nil elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then return redis.call('DEL',KEYS[1]) else return 0 end"
微服务整合
private final static String REDIS_KEY = "rdl_sale_lock";
private final static Long EXPIRE_ = 30L;
private Lock lock = new RedisDistributedLock(redisTemplate,REDIS_KEY,EXPIRE_);
public String sale() {
String message = "";
lock.lock();
try {
message = excuteSale(message);
} finally {
lock.unlock();
}
System.out.println(message);
return message;
}
/**
* 自研Redis 分布式锁
*
* @author :liao.wei
* @date :2023/10/1 21:03
* @package : com.mco.rdl.lock
*/
public class RedisDistributedLock implements Lock {
private StringRedisTemplate stringRedisTemplate;
/**
* 锁名 KEYS[1]
*/
private String lockKey;
/**
* 锁值 ARGV[1]
*/
private String lockVal;
/**
* 过期时间 ARGV[2]
*/
private long expireTime;
public RedisDistributedLock(StringRedisTemplate stringRedisTemplate, String lockKey, String lockVal, long expireTime) {
this.stringRedisTemplate = stringRedisTemplate;
this.lockKey = lockKey;
this.expireTime = expireTime;
this.lockVal = lockVal + ":" + Thread.currentThread().getId();
}
@Override
public void lock() {
tryLock();
}
@Override
public boolean tryLock() {
try {
return tryLock(-1, TimeUnit.SECONDS);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
if (time == -1L) {
String script = "if redis.call('EXISTS',KEYS[1]) == 0 or redis.call('HEXISTS',KEYS[1],ARGV[1]) == 1 then " +
"redis.call('HINCRBY',KEYS[1],ARGV[1],1) " +
"redis.call('EXPIRE',KEYS[1],ARGV[2]) " +
"return 1 " +
"else " +
"return 0 " +
"end";
System.out.println("lockname=" + lockKey + ",uuidval=" + lockVal);
while (Boolean.FALSE.equals(stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Collections.singletonList(lockKey), lockVal, StrUtil.toString(expireTime)))) {
// 暂停 60毫秒,进行递归重试
try {
TimeUnit.MILLISECONDS.sleep(60);
} catch (InterruptedException e) {
Log.get().log(Level.ERROR, String.format("分布式锁(%s->%s)重试失败!", lockKey, lockVal));
}
}
return true;
}
return false;
}
@Override
public void unlock() {
System.out.println("释放 lockname=" + lockKey + ",uuidval=" + lockVal);
String script = "if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then " +
"return nil " +
"elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then " +
"return " +
"redis.call('DEL',KEYS[1]) " +
"else " +
"return 0 " +
"end";
Long flag = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Collections.singletonList(lockKey), lockVal);
if (Objects.isNull(flag)) {
throw new RuntimeException(String.format("%s当前锁%s ===%s 不存在!/(ㄒoㄒ)/~~", flag, lockKey, lockVal));
}
}
/**
* 下面两个暂时不实现
*/
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public Condition newCondition() {
return null;
}
}
分布式锁引入工厂模式
/**
* 分布式锁工厂类
*
* @author :liao.wei
* @date :2023/10/1 22:11
* @package : com.mco.rdl.lock
*/
@Component
@RequiredArgsConstructor
public class DistributedLockFactory {
private final StringRedisTemplate template;
public Lock getDistributeLock(String lockType, String lockKey, String uuid, Long expire) {
if (StrUtil.isEmpty(lockType)) {
return null;
} else if (StrUtil.equalsIgnoreCase("REDIS", lockType)) {
return new RedisDistributedLock(template, lockKey, uuid, expire);
} else if (StrUtil.equalsIgnoreCase("ZOOKEEPER", lockType)) {
// TODO zookeeper版本分布式锁
return null;
} else if (StrUtil.equalsIgnoreCase("MYSQL", lockType)) {
// TODO MYSQL版本分布式锁
return null;
} else {
return null;
}
}
}
测试
private String excuteSale(String uuid) {
String message = "";
// 查询库存
String sales = redisTemplate.opsForValue().get(KEY_);
//判断库存是否足够
Integer restSale = StrUtil.isEmpty(sales) ? null : Integer.parseInt(sales);
//扣减数量
if (Objects.nonNull(restSale) && restSale > 0) {
redisTemplate.opsForValue().set(KEY_, String.valueOf(--restSale));
message = String.format("卖出商品,库存剩余 %s,服务端口%s", restSale, port);
testEntry(uuid);
} else {
message = String.format("商品已售罄,服务端口%s", port);
}
return message;
}
private void testEntry(String uuid) {
Lock lock = distributedLockFactory.getDistributeLock("Redis", REDIS_KEY, uuid, EXPIRE_);
lock.lock();
try {
System.out.println("---------测试可重入锁------");
} finally {
lock.unlock();
}
}
自动续期
业务执行时间大于过期时间
CAP
Redis集群 | Zookeeper集群 | Eureka集群 | Nacos集群 |
AP | CP | AP | AP |
Lua 脚本
/**
* 启动定时任务进行自动续期
*/
private void reNewExpire() {
String script = "if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 1 then " +
"return redis.call('EXPIRE',KEYS[1],ARGV[2]) " +
"else " +
"return 0 " +
"end";
new Timer().schedule(new TimerTask() {
@Override
public void run() {
if (Boolean.TRUE.equals(stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Collections.singletonList(lockKey), lockVal, StrUtil.toString(expireTime)))) {
reNewExpire();
}
}
}, this.expireTime * 1000 / 3);
}