Setnx:不存在则设置
public String deductStock(){
String lockKey="lock:product:101";
Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, “zyiscool“);
if (!aBoolean){
return "biz_code";
}
String stockValue = (String) stringRedisTemplate.opsForValue().get("stock");
if (stockValue == null) {
System.out.println("库存信息不存在");
throw new RuntimeException("库存信息不存在无法下单");
}
int stock = Integer.parseInt(stockValue);
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", String.valueOf(realStock));
System.out.println("扣减成功,剩余库存:" + realStock);
return "ok";//返回类型根据业务自己设置
} else {
System.out.println("扣减失败,库存不足");
return "error";//返回类型根据业务自己设置
}
}
String lockKey= "lock:product:101";
redisTemplate.opsForValue().setIfAbsent(lockKey,"zyiscool");
redisTemplate.delete(lockKey);
这种简单的锁距离项目实际上用,还差十万八千里!
下面问的都是上面代码的问题:
例如:问:如果中间代码抛出了异常怎么办?
答:加finally,
问:那如果服务崩了,还没有执行finally中的代码,你怎么办?
答:为setIfAbsent设置过期时间:
redisTemplate.opsForValue().setIfAbsent(
lockKey,
"zyiscool",
Duration.ofDays(TimeUnit.SECONDS.toSeconds(10))
);
也可以这么写:
redisTemplate.opsForValue().setIfAbsent(
lockKey,
"zyiscool,
10,
TimeUnit.SECONDS
);
重点:一定要一行完成。
public String deductStock(){
String lockKey="lock:product:101";
Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "zyiscool", 10, TimeUnit.SECONDS);
if (!aBoolean){
return "biz_code";
}
try {
String stockValue = (String) stringRedisTemplate.opsForValue().get("stock");
if (stockValue == null) {
System.out.println("库存信息不存在");
throw new RuntimeException("库存信息不存在无法下单");
}
int stock = Integer.parseInt(stockValue);
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", String.valueOf(realStock));
System.out.println("扣减成功,剩余库存:" + realStock);
return "ok";//返回类型根据业务自己设置
} else {
System.out.println("扣减失败,库存不足");
return "error";//返回类型根据业务自己设置
}
} catch (Exception e) {
e.printStackTrace();
return "error"; // 捕获异常
}
finally {
stringRedisTemplate.delete(lockKey);
}
这些写其实没问题,问题不大,但是如果是高并发环境下,那么就有很多的问题:
例如:
在高并发下,设置分布式锁设置10秒钟超时。
在请求中不断有新请求对101商品上加锁。加锁被别的请求给释放掉,导致锁失效
步骤是这样的:在极端环境下,也就是高并发的环境下,被本来锁是10秒钟过期的。结果线程1运行到11秒还没有运行完,11秒时锁就已经失效了,然后线程1到15秒时,然后线程一干了一件蠢事,把锁给删掉了,这删掉的锁时线程二的,因为在线程一上锁10秒后锁过期了然后线程二就加锁成功了,线程二执行了这段代码用了8秒,重点来了,线程二执行到5秒的时候这时候锁已经被线程1给删了,然后线程二执行到8秒的时候,线程二把线程三的锁给删了。
这就导致了锁失效。导致后面一直有请求进行扣减库存,然后就会导致超卖的问题,这个bug非常大!(轻则扣工资,重则让你赔的倾家荡产(如果你有钱当我没说))
问题代码:
stringRedisTemplate.delete(lockKey);//我释放了别人的锁
解决步骤,在finally中判断是不是自己加的锁,是就删,不是就不删
解决问题后的代码:
public String deductStock(){
String lockKey="lock:product:101";
String clientId = UUID.randomUUID().toString();
Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 10, TimeUnit.SECONDS);
if (!aBoolean){
return "biz_code";
}
try {
String stockValue = (String) stringRedisTemplate.opsForValue().get("stock");
if (stockValue == null) {
System.out.println("库存信息不存在");
throw new RuntimeException("库存信息不存在无法下单");
}
int stock = Integer.parseInt(stockValue);
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", String.valueOf(realStock));
System.out.println("扣减成功,剩余库存:" + realStock);
return "ok";//返回类型根据业务自己设置
} else {
System.out.println("扣减失败,库存不足");
return "error";//返回类型根据业务自己设置
}
} catch (Exception e) {
e.printStackTrace();
return "error"; // 捕获异常
}
finally {
if(clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))){
stringRedisTemplate.delete(lockKey);
}
}
}
}
继续分析代码:
下面finally代码有没有问题?
//如果是自己加的锁,那么就删除,不是就不删
if (clientId.equals(redisTemplate.opsForValue().get(lockKey))){
redisTemplate.delete(lockKey);
}
有什么问题?
答:不是原子性,原子性是什么?
是代码的最小分割单位,大白话:就是要在一行代码中完成。
下面代码中:
finally {
if (clientId.equals(redisTemplate.opsForValue().get(lockKey))){
redisTemplate.delete(lockKey);
}
}
在极端环境下,比如redis请求卡了,
redisTemplate.opsForValue().get(lockKey)
此时时间已经是9.900秒了,服务器网络波动导致获取锁正在查询,此时已经到9.999秒了然后查出来了,线程一接着在finally中进行比较没问题(比较完成了用了0.001秒,正好到10秒钟,然后线程二成功加上了锁),这时线程一执行删除命令。删除命令执行的时候,这下直接完蛋!这删的就是线程二加的锁,然后就引发à锁失效。然后导致超卖。(这么写的绝对是大聪明)
下面是原子性的详细介绍:
原子性:原子性是指一个操作要么完全执行,要么完全不执行,不能被中断。在这个例子中redisTemplate.opsForValue().get(lockKey) 和 redisTemplate.delete(lockKey) 这两个操作并不是原子性的。如果在获取锁的过程中,另一个线程已经成功获取了锁并执行了相关操作,那么在你删除锁时就可能会出现问题。
解决问题:
源代码:
finally {
if (clientId.equals(redisTemplate.opsForValue().get(lockKey))){
redisTemplate.delete(lockKey);
}
}
//我们需要改变的代码:
if (clientId.equals(redisTemplate.opsForValue().get(lockKey))){
redisTemplate.delete(lockKey);
}
这两行代码如果把放一行同时执行,就是进行原子性执行。
这个我们可以使用lua脚本来执行。
代码:
public class MyRedisScript implements RedisScript<Long> {
private final String script;
public MyRedisScript(String script) {
this.script = script;
}
@Override
public String getSha1() {
// 可以实现计算 SHA-1 的逻辑,或者返回 null
return null;
}
@Override
public Class<Long> getResultType() {
return Long.class;
}
@Override
public String getScriptAsString() {
return this.script;
}
}
修改方法中的判断的方法
改成这样:
String luaScript =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
RedisScript<Long> redisScript = new MyRedisScript(luaScript);
Long result = stringRedisTemplate.execute(redisScript, Collections.singletonList(lockKey), clientId);
代码解释:
lockKey解释:这是你上的分布式锁:你上锁的时候是不是指定了商品id? 就是这个"lock:product:101"
clientId解释:你的客户端id,就是这个UUID.randomUUID().toString();要判断