目录
场景描述
订单扣减场景举例
代码调整1
代码调整2
代码调整3
redisson锁续命核心代码
//首先在redis中set stock 300
@RequestMapping("/deduct_stock")
public String deductStock() {
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("扣减失败,库存不足");
}
return "end";
}
以上场景肯定会出现并发问题,当有多个用户同时进行库存扣减的时候,可能在获取stock数量的时候获取到相同的值,有可能会出现此时只有一件库存,但是三个用户下单,出现库存超卖问题。
@RequestMapping("/deduct_stock")
public String deductStock() {
synchronized (this){
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("扣减失败,库存不足");
}
return "end";
}
}
通过增加synchronize锁可以解决并发问题,但是只对单机有效。如果多服务器之间也会出现并发超卖问题。
@RequestMapping("/deduct_stock")
public String deductStock() {
String lockKey = "lock:product_101";
//通过redis的setnx命令来模拟一把分布式锁,并设置超时时间,该指令是原子操作
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "101", 30, TimeUnit.SECONDS); //jedis.setnx(k,v)
//如果设置失败,result会返回false.如果成功走正常扣减订单逻辑。
if (!result) {
return "error_code";
}
try {
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("扣减失败,库存不足");
}
return "end";
//如果业务代码中出现异常,也要保证锁的释放,也就是setnx的删除操作,避免死锁
} finally {
stringRedisTemplate.delete(lockKey);
}
}
以上代码通过setnx的方式在并发量不高的时候,可能没有什么问题,如果并发量较高,某个线程获取到锁有执行业务代码超过了设置的超时时间,就会有并发问题发生了。假设线程1执行完成该方法用时15秒,执行到10秒的时候,因为超时时间将锁释放了,此时线程2获取到锁并执行业务逻辑,执行过程中,线程1执行完业务,并通过finally又释放了一次锁,可此时线程2不一定执行完。这种情况就会出现严重的并发问题。
通过上述代码会发现,造成该问题的主要因素是超时时间的问题,为了解决该问题使用redisson锁续命来完善代码。
引入redisson依赖
org.redisson
redisson
3.6.5
在启动类中配置
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@Bean
public Redisson redisson() {
// 单机模式
Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379").setDatabase(0);
return (Redisson) Redisson.create(config);
}
}
订单接口调整
@Autowired
private Redisson redisson;
@RequestMapping("/deduct_stock")
public String deductStock() {
String lockKey = "lock:product_101";
//获取锁对象
RLock redissonLock = redisson.getLock(lockKey);
//加分布式锁
redissonLock.lock(); // .setIfAbsent(lockKey, "101", 30, TimeUnit.SECONDS);
try {
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 "end";
}
通过使用redisson会自动每隔10秒检查是否还持有锁,如果持有锁就延长锁的时间,默认延长30秒。
private void scheduleExpirationRenewal(final long threadId) {
if (!expirationRenewalMap.containsKey(this.getEntryName())) {
Timeout task = this.commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
public void run(Timeout timeout) throws Exception {
RFuture future = RedissonLock.this.commandExecutor.evalWriteAsync(
RedissonLock.this.getName(),
LongCodec.INSTANCE,
RedisCommands.EVAL_BOOLEAN,
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) " +
"then redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return 1; end; return 0;",
Collections.singletonList(RedissonLock.this.getName()),
new Object[]{RedissonLock.this.internalLockLeaseTime,
RedissonLock.this.getLockName(threadId)});
future.addListener(new FutureListener() {
public void operationComplete(Future future) throws Exception {
RedissonLock.expirationRenewalMap.remove(RedissonLock.this.getEntryName());
if (!future.isSuccess()) {
RedissonLock.log.error("Can't update lock " +
RedissonLock.this.getName() +
" expiration", future.cause());
} else {
if ((Boolean) future.getNow()) {
RedissonLock.this.scheduleExpirationRenewal(threadId);
}
}
}
});
}
}, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS);
if (expirationRenewalMap.putIfAbsent(this.getEntryName(), task) != null) {
task.cancel();
}
}
}