自学记录
模拟库存扣减的场景
Redis使用的数据结构是string
key为stock,初始的value为298
初始代码:
@GetMapping("/deduct_stock1")
public String deductStack1() {
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("扣减失败,库存不足");
}
return "end";
}
问题:可能出现超卖现象。
例如有三个用户并发执行,三个用户都获取了stock的值后进入if判断语句,现在三者获得的stock都是初始值298,那么该业务执行后stock的值是297,但是实际上还多卖给了两个用户,出现超卖现象。
解决方式:使用synchronized
关键字
synchronized
关键字的作用是创建一个同步块,用于对共享资源进行同步控制。它的作用是确保同一时刻只有一个线程可以进入被同步的代码块,从而防止多个线程同时访问或修改共享资源,保证了线程安全性。
@GetMapping("/deduct_stock1")
public String deductStack1() {
synchronized (this) {
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("扣减失败,库存不足");
}
}
return "end";
}
这样改进后看似解决了刚刚提到的超卖问题,但是实际上治标不治本,问题仍然存在。
问题:在实际的业务场景中,会有多个服务器,但是synchronized只对所处的服务中生效,无法影响其他的服务中的业务,所以,当上述问题不是发生在单体项目中,而是发生在多服务项目中,使用synchronized仍然会产生超卖现象
解决方法:使用分布式锁
使用商品id来作为分布式锁的key,使用stringRedisTemplate内置setIfAbsent方法来实现分布式锁,原理等于setnx(k, v),如果redis中有k,则返回false,没有k,则创建分布式锁,返回true。在执行完扣减库存的业务后需要删除分布式锁,给别的线程放行。
@GetMapping("/deduct_stock1")
public String deductStack1() {
//此处是模拟,实际情况中商品id应该是通过方法中的参数传递过来
String lockKey = "productId";
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "block");
if (!result) {
return "error";
}
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("扣减失败,库存不足");
}
stringRedisTemplate.delete(lockKey);
return "end";
}
接下来考虑可能出现异常的情况
问题:当第一个持有锁的用户在没有删除锁之前出现异常了,那么就无法释放分布式锁,则别的线程就无法获取分布式锁,从而导致很多用户无法扣减库存。
解决方法:使用try-catch-finally,将删除锁的逻辑放在finally中,则一定会执行删除锁的逻辑
@GetMapping("/deduct_stock1")
public String deductStack1() {
//此处是模拟,实际情况中商品id应该是通过方法中的参数传递过来
String lockKey = "productId";
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "block");
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 {
stringRedisTemplate.delete(lockKey);
}
return "end";
}
接下来考虑宕机的情况
问题:当在执行扣减库存的业务的时候,出现了宕机的情况,哪怕有finally也无法执行删除分布式锁的逻辑
解决方法:为分布式锁设定一个超时时间,到了超时时间,则redis自动删除锁。
@GetMapping("/deduct_stock1")
public String deductStack1() {
//此处是模拟,实际情况中商品id应该是通过方法中的参数传递过来
String lockKey = "productId";
/*
* 注意:
* 不能先创建锁,再设置超时时间,一定要创建锁的同时,就设置超时时间
* 因为,如果分开来设置,在创建锁和设置超时时间中间出现了宕机或异常,仍然无法释放锁
*/
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "block", 10, 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 {
stringRedisTemplate.delete(lockKey);
}
return "end";
}
接下里考虑高并发的场景
问题:出现当前获取锁的用户需要的执行时间大于分布式锁的超时时间,自动释放后,下一个用户获取分布式锁,而当前用户会执行释放锁的操作,就把下一个用户获取的锁释放了,以此类推。如图:
解决方法:将分布式锁的value设置为UUID,每次删除之前,判断当前锁的value与执行删除锁逻辑的线程的UUID是否相同,不同则等待,相同则删除
@GetMapping("/deduct_stock1")
public String deductStack1() {
//此处是模拟,实际情况中商品id应该是通过方法中的参数传递过来
String lockKey = "productId";
/*
* 注意:
* 不能先创建锁,再设置超时时间,一定要创建锁的同时,就设置超时时间
* 因为,如果分开来设置,在创建锁和设置超时时间中间出现了宕机或异常,仍然无法释放锁
*/
String clientId = UUID.randomUUID().toString();
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 10, 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";
}
这样的情况似乎以及没问题了,但是仔细思考一下,其实仍然存在问题
问题:如果当前逻辑执行到finally中的if语句后,但此时分布式锁超时了,同时下一个用户正好获取了锁,则当前逻辑删除的分布式锁是下一个用户获取的。
解决方法:通过锁续命的操作来解决这一问题,锁续命:如果持有锁的进程或线程在执行任务时需要更多时间,它可以周期性地(通常是在锁即将过期时)发送一个续命请求,以更新锁的过期时间。redisson封装该实现。
@GetMapping("/deduct_stock1")
public String deductStack1() {
//此处是模拟,实际情况中商品id应该是通过方法中的参数传递过来
String lockKey = "productId";
/*
* 注意:
* 不能先创建锁,再设置超时时间,一定要创建锁的同时,就设置超时时间
* 因为,如果分开来设置,在创建锁和设置超时时间中间出现了宕机或异常,仍然无法释放锁
*/
String clientId = UUID.randomUUID().toString();
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 10, TimeUnit.SECONDS);
RLock redissonLock = redisson.getLock(lockKey);
redissonLock.lock();
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 {
redissonLock.unlock();
}
return "end";
}
暂时就优化到这里,随着进一步的学习,或许会有更多需要优化的地方,欢迎批评指正