分布式-Redis03-分布式锁实战

分布式锁实战

分布式锁应用场景

1、互联网秒杀

2、抢优惠券

3、接口幂等性校验

redis实现分布式锁

1.0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@GetMapping("/deduct_stock")
public String deductStock() throws InterruptedException {
String lockKey = "product_001";
String locKValue = "product_001";

Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, locKValue);
if(!lock){
return "fail";
}
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
System.out.println("扣减成功,剩余库存:" + realStock + "");
} else {
System.out.println("扣减失败,库存不足");
}

stringRedisTemplate.delete(lockKey);
return "success";
}

存在问题:

1、没有设置锁过期时间

2、锁不保证得到释放

2.0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@GetMapping("/deduct_stock_2")
public String deductStock2() throws InterruptedException {
String lockKey = "product_001";
String locKValue = "product_001";

try {
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, locKValue,15, TimeUnit.SECONDS);
if(!lock){
return "fail";
}
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
System.out.println("扣减成功,剩余库存:" + realStock + "");
} else {
System.out.println("扣减失败,库存不足");
}
}finally {
stringRedisTemplate.delete(lockKey);
}
return "success";
}

存在问题:

1、高并发场景下,可能释放了别人的锁

3.0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@GetMapping("/deduct_stock_3")
public String deductStock3() throws InterruptedException {
String lockKey = "product_001";
String locKValue = "product_001";
locKValue = UUID.randomUUID().toString();

try {
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, locKValue,15, TimeUnit.SECONDS);
if(!lock){
return "fail";
}
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
System.out.println("扣减成功,剩余库存:" + realStock + "");
} else {
System.out.println("扣减失败,库存不足");
}
}finally {
if(locKValue.equals(stringRedisTemplate.opsForValue().get(lockKey))){
stringRedisTemplate.delete(lockKey);
}
}
return "success";
}

存在问题:

1、释放锁时,存在非原子操作

对于释放锁时的非原子操作问题,可以使用 Redis 的 Lua 脚本确保释放锁的操作是原子性(Lua脚本内能保证redis命令连续且完整的执行)的。例如:

1
2
3
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
Long result = stringRedisTemplate.execute(redisScript, Arrays.asList(lockKey),locKValue);

2、线程1还没跑完,锁就释放了,线程2获取到锁,分布式锁失效(不知道该设置锁超时时间多少合适)

解决办法:

1、使用已经封装好的Redisson,自动续期key失效时间

2、合理定义超时时间,减少耗时,且设置锁过期时间稍稍长一点

3、自己实现第一点逻辑,采用“看门狗”机制:启动一个单独的线程或定时任务,在锁快要过期时自动为锁续期,只要业务操作还未完成

4、监控和告警:在finally之前获取锁成功,释放锁失败时,告警异常情况。

4.0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@GetMapping("/deduct_stock_4")
public String deductStock4() throws InterruptedException {
String lockKey = "product_001";
RLock redissonLock = redisson.getLock(lockKey);
try {
redissonLock.lock();
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
System.out.println("扣减成功,剩余库存:" + realStock + "");
} else {
System.out.println("扣减失败,库存不足");
}
}finally {
redissonLock.unlock();
}
return "success";
}

存在问题:

1、其他线程获取不到锁超过一定时间,获取锁失败

解决办法:设置获取锁尝试时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@GetMapping("/deduct_stock_5")
public String deductStock5() throws InterruptedException {
String lockKey = "product_001";
RLock redissonLock = redisson.getLock(lockKey);
try {
redissonLock.tryLock(10,TimeUnit.SECONDS);
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
System.out.println("扣减成功,剩余库存:" + realStock + "");
} else {
System.out.println("扣减失败,库存不足");
}
}finally {
redissonLock.unlock();
}
return "success";
}

Redisson分布式锁实现原理

image-20220413213955415

Redis实现分布式的问题

1、redis集群元数据的维护采用gossip协议通信

master消息没同步给slave前,master挂了,导致另一线程获取锁也成功

image-20220413214958142

RedLock

所有节点获取锁成功才成功,优点类似zookeeper(过半节点成功才成功)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@RequestMapping("/redlock")
public String redlock() throws InterruptedException {
String lockKey = "product_001";
//这里需要自己实例化不同redis实例的redisson客户端连接,这里只是伪代码用一个redisson客户端简化了
RLock lock1 = redisson.getLock(lockKey);
RLock lock2 = redisson.getLock(lockKey);
RLock lock3 = redisson.getLock(lockKey);

/**
* 根据多个 RLock 对象构建 RedissonRedLock (最核心的差别就在这里)
*/
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
try {
/**
* 4.尝试获取锁
* waitTimeout 尝试获取锁的最大等待时间,超过这个值,则认为获取锁失败
* leaseTime 锁的持有时间,超过这个时间锁会自动失效(值应设置为大于业务处理的时间,确保在锁有效期内业务能处理完)
*/
boolean res = redLock.tryLock(10, 30, TimeUnit.SECONDS);
if (res) {
//成功获得锁,在这里处理业务
}
} catch (Exception e) {
throw new RuntimeException("lock fail");
} finally {
//无论如何, 最后都要解锁
redLock.unlock();
}

return "end";
}

分布锁的三种实现

1、redis实现分布式锁(redisson)

2、Zookeeper实现分布式锁(集中式集群管理,性能较差)

3、基于数据库实现分布式锁

1、利用唯一索引(lock:借助唯一索引保证插入某个key的记录成功 unlock:删除记录)

2、行锁-利用乐观锁 update set xxx= xxx where xxx=xxx,根据返回的影响行数来判断是否获取锁成功 (https://blog.csdn.net/qq_19801061/article/details/119146521)

分布式锁总结

在目前分布式锁实现方案中,比较成熟、主流的方案有两种:

(1)基于Redis的分布式锁:适合并发量高,性能要求很高,对数据一致性要求较低,允许最终一致性的场景

(2)基于ZooKeeper的分布式锁:适合对数据一致性要求较高且并发量较低的场景

基于jedis实现分布式锁