分布式锁解决方案之Reids实现分布式锁,完成秒杀功能
前言
当数据量大,并发量高时,我们需要使用分布式锁,比如互联网秒杀商品,抢优惠卷、接口幂等性校验等。
分布式锁-Redisson
一、分布式锁有哪些解决方案?
1.1 基于Reids实现分布式锁
① setnx key value
②Redisson
1.2 基于Zookeeper实现分布式锁
主要是利用顺序节点或临时节点。
临时节点:我的客户端和zookeeper建立连接之后,这个节点一直生效,那么别人来建立连接,这个连接已经有了,必须等我断开连接。当客户端和zookeeper的连接断开了之后,这个节点就自动消失了。
顺序节点:A用户来和Zookeeper建立连接,就是A节点;B用户来,就是B节点。A节点拿到了锁,B节点要等A节点执行完业务释放锁之后才能拿到分布式锁。
1.3 基于数据库实现分布式锁
比如Mysql,利用主键或唯一索引的唯一性。
二、Reids实现之setnx key value 10s
2.1原理
setnx的作用是存入一个不存在的字符串键值对。即:如果原来有这个key,就存入失败。
当有多个用户来获取这个锁,可以保证锁的互斥性,当我拿到这个锁了之后,别人就拿不到这个锁。
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId);//setnx key value
2.2 可能会出现的问题和解决方案
1)分布式锁失效的问题
我们设置一个锁,执行完业务逻辑,使用完之后,一定要释放锁,不然别人拿不到这个锁。
场景一:我执行完业务逻辑,还没执行到释放锁的时候,程序挂了,那么这个锁就一直存在redis,别人就拿不到这个锁。
解决方案:我们就要给key设置过期时间,一般设置30s,30s之后就过期。这样别人就可以拿到这个锁了。
stringRedisTemplate.expire(lockKey,30,TimeUnit.SECONDS);//expire key 30s
2)过期时间问题
场景二:使用expire key 30s来设置时间是不行的,如果程序在设置锁之后和加过期时间之前挂了,这个过期时间就没设置上。
解决方案:我们要把setnx和expire key做成原子性的。
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 30, TimeUnit.SECONDS);//setnx key value ex 30 s
3)删除锁问题
场景三:删除锁的时候需要判断是不是我的锁,首先getkey的value是唯一的,用uuid来做就可以了。
场景四:设置锁的过期时间是30s,我要执行35s,在程序的最后5s内,别人也成功设置了一个锁。然后我在第5s就把别人的锁删除了,这样删除的锁就是不是我拿到的那个锁了。
场景五:我设置的key过期时间是30s,结果我程序执行了35s,程序还没执行完,锁就释放了,别人就可以拿到锁。那么两个程序都进入到了同一个方法中,会导致数据不一致的问题,可能会超卖。
解决方案:看门狗机制,watch dog :设置key的过期时间是30s,每10s判断key在不在,如果在就续时长到30s。
这样就可以防止,我的程序还没执行完而我的key就过期了,导致别人拿到我的锁。
@Autowired
private StringRedisTemplate stringRedisTemplate;
public String deductStock() {
String lockKey = "lockKey";
String clientId = UUID.randomUUID().toString();
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 30, TimeUnit.SECONDS);
if (!result) {
return "error";//没有拿到锁就返回
}
try {
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if (stock > 0){
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock",realStock+"");
System.out.println("扣减商品库存成功,剩余库存"+realStock);
} else {
System.out.println("扣减商品库存失败,库存不足");
}
}finally {
if (clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))) {
stringRedisTemplate.delete(lockKey);
}
}
return "end";
}
三、Reids实现之Redisson
Redisson是Redis官方推荐的Java版的Redis客户端。它主要应用于分布式场景,此处我们只用它的分布式锁功能。
我们主要是利用lock和unlock。
@Autowired
private Redisson redisson;
public String deductStock(){
String lockKey = "lockKey";
RLock redissonLock = redisson.getLock(lockKey);//拿到锁的对象
try {
redissonLock.lock();//加锁
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//拿库存
if (stock > 0){
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock",realStock+"");
System.out.println("扣减商品库存成功,剩余库存"+realStock);
} else {
System.out.println("扣减商品库存失败,库存不足");
}
}finally {
redissonLock.unlock();//释放锁
}
return "end";
}