今天记录学习Redis分布式的相关功能。
在公司写业务代码的时候,遇到超卖的问题,接下来就慢慢的探讨这个问题的解决方案。
在单体的公司中,我们可以使用synchronized解决相关问题。代码如下:
一:使用synchronized
@RequestMapping(value = "buy_Goods1", method = RequestMethod.GET)
public String buy_Goods1() {
synchronized (this) {
String key = "lock:good_101";
String resultRedisTotal = (String) redisTemplate.opsForValue().get(key);
Integer totalNum = Integer.parseInt(resultRedisTotal);
if (totalNum > 0) {
int realNum = totalNum - 1;
redisTemplate.opsForValue().set(key, String.valueOf(realNum));
return "购买成功" + realNum;
} else {
System.out.println("商品已经售完");
}
return "商品已经售完";
}
}
随着微服务的兴起,上述代码在微服务就无法完成该功能了。于是我们就想到了redis可以实现分布式锁的功能。利用它内部的setNx命令
二:使用分布式锁
@RequestMapping(value = "buy_Goods2", method = RequestMethod.GET)
public String buy_Goods2() {
// 分布式锁
String value = Thread.currentThread().getName();
Boolean flag = redisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value);// 相当于setNx
if (!flag) {
return "抢锁失败";
}
String key = "lock:good_101";
String resultRedisTotal = (String) redisTemplate.opsForValue().get(key);
Integer totalNum = Integer.parseInt(resultRedisTotal);
if (totalNum > 0) {
int realNum = totalNum - 1;
redisTemplate.opsForValue().set(key, String.valueOf(realNum));
redisTemplate.delete(REDIS_LOCK);
return "购买成功";
} else {
System.out.println("商品已经售完");
}
return "商品已经售完";
}
以上就会发现一个问题,如果在执行业务代码报错了,没有执行到delete删除锁的时候,就会出现该锁一直在的问题。
三:程序报错也最终解锁
@RequestMapping(value = "buy_Goods3", method = RequestMethod.GET)
public String buy_Goods3() {
// 分布式锁
String value = Thread.currentThread().getName();
try {
Boolean flag = redisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value);// 相当于setNx
if (!flag) {
return "抢锁失败";
}
String key = "lock:good_101";
String resultRedisTotal = (String) redisTemplate.opsForValue().get(key);
Integer totalNum = Integer.parseInt(resultRedisTotal);
if (totalNum > 0) {
int realNum = totalNum - 1;
redisTemplate.opsForValue().set(key, String.valueOf(realNum));
return "购买成功";
} else {
System.out.println("商品已经售完");
}
} catch (Exception e) {
} finally {
redisTemplate.delete(REDIS_LOCK);
}
return "商品已经售完";
}
以上改造,依旧会存在一个问题,如果该程序被kill掉,那么在redis的锁还会一直在,为了解决该问题,我们也需要设置redis的过期时间
四:设置redis锁的过期时间
@RequestMapping(value = "buy_Goods4", method = RequestMethod.GET)
public String buy_Goods4() {
// 分布式锁
String value = Thread.currentThread().getName();
try {
// 原子性
Boolean flag = redisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value, 5, TimeUnit.SECONDS);// 相当于setNx
if (!flag) {
return "抢锁失败";
}
String key = "lock:good_101";
String resultRedisTotal = (String) redisTemplate.opsForValue().get(key);
Integer totalNum = Integer.parseInt(resultRedisTotal);
if (totalNum > 0) {
int realNum = totalNum - 1;
redisTemplate.opsForValue().set(key, String.valueOf(realNum));
return "购买成功";
} else {
System.out.println("商品已经售完");
}
} catch (Exception e) {
} finally {
redisTemplate.delete(REDIS_LOCK);
}
return "商品已经售完";
}
再次改造后,还是会发现一个问题。就是在删除锁的时候,如果A进行卡顿了,被redis过期时间删除,B线程进入代码,在finally中,就会被删掉。
五:防止删除别的线程的锁
@RequestMapping(value = "buy_Goods5", method = RequestMethod.GET)
public String buy_Goods5() {
// 分布式锁
String value = Thread.currentThread().getName();
try {
// 原子性
Boolean flag = redisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value, 5, TimeUnit.SECONDS);// 相当于setNx
if (!flag) {
return "抢锁失败";
}
String key = "lock:good_101";
String resultRedisTotal = (String) redisTemplate.opsForValue().get(key);
Integer totalNum = Integer.parseInt(resultRedisTotal);
if (totalNum > 0) {
int realNum = totalNum - 1;
redisTemplate.opsForValue().set(key, String.valueOf(realNum));
return "购买成功";
} else {
System.out.println("商品已经售完");
}
} catch (Exception e) {
} finally {
// 解锁
if (redisTemplate.opsForValue().get(REDIS_LOCK).equals(value)) {
redisTemplate.delete(REDIS_LOCK);
}
}
return "商品已经售完";
}
经过以上改造,我们又会发现一个问题,finally块的代码和del的删除操作不是一个原子性的,依旧会出现问题。
六:解决finally块和删除操作的原子性(使用lua脚本)
众所周知,lua脚本是可以实现原子性的操作的。
String script = "if redis.call('get',KEYS[1]) == ARGV[1]" +
"then " +
"return redis.call('del',KEYS[1])" +
"else" +
" return 0" +
"end";
另一种方式,使用redis本事的事务特性,完成该操作。
while (true){
redisTemplate.watch(REDIS_LOCK);
if(redisTemplate.opsForValue().get(REDIS_LOCK).equals(value)){
redisTemplate.setEnableTransactionSupport(true);
redisTemplate.multi();
redisTemplate.delete(REDIS_LOCK);
List<Object> list = redisTemplate.exec();
if(list == null){
continue;
}
}
redisTemplate.unwatch();
break;
}
以上的改造,我们就可以基本上完成一个分布式锁的超卖问题。
随着Redis的发展,给我们封装了更好的解决方案。解决了Redis中的缓存续命问题。
七:使用Redisson
@RequestMapping(value = "buy_Goods7", method = RequestMethod.GET)
public String buy_Goods7() {
String key = "lock:good_101";
String value = Thread.currentThread().getName();
RLock redissonLock = redisson.getLock(REDIS_LOCK);
redissonLock.lock();
try{
String resultRedisTotal = (String) redisTemplate.opsForValue().get(key);
Integer totalNum = Integer.parseInt(resultRedisTotal);
if (totalNum > 0) {
int realNum = totalNum - 1;
redisTemplate.opsForValue().set(key, String.valueOf(realNum));
return "购买成功" + realNum;
} else {
System.out.println("商品已经售完");
}
}finally {
redissonLock.unlock();;
}
return "商品已经售完";
}
以上的Redisson自己封装了Lua脚本的解决方案,对我们业务使用者来说,三行代码即可。
以上还是在大量高并发的情况下,还是会有一些问题的存在,可能会报错:attempt to unlock lock,not locked by current thread by node id;
八:使用Redission解决解锁的时候高并发问题
@RequestMapping(value = "buy_Goods8", method = RequestMethod.GET)
public String buy_Goods8() {
String key = "lock:good_101";
String value = Thread.currentThread().getName();
RLock redissonLock = redisson.getLock(REDIS_LOCK);
redissonLock.lock();
try{
String resultRedisTotal = (String) redisTemplate.opsForValue().get(key);
Integer totalNum = Integer.parseInt(resultRedisTotal);
if (totalNum > 0) {
int realNum = totalNum - 1;
redisTemplate.opsForValue().set(key, String.valueOf(realNum));
return "购买成功" + realNum;
} else {
System.out.println("商品已经售完");
}
}finally {
// 解锁 避免报错 attempt to unlock lock,not locked by current thread by node id;
if(redissonLock.isLocked()){
if(redissonLock.isHeldByCurrentThread()){
redissonLock.unlock();
}
}
}
return "商品已经售完";
}
以上就是在使用Redis作为分布式锁的过程中,一步步进行项目的优化,处理对应的问题,找到最合适的分布式锁。