以下代码示例在并发情况下存在超卖情况:
@RestController
public class OverSell {
@Autowired
private RedisTemplate redisTemplate;
@GetMapping("/overSell")
public String overSell(@RequestParam("key") String key) {
Integer sell = (Integer) redisTemplate.opsForValue().get(key);
sell = sell - 1;
if (sell > 0) {
redisTemplate.opsForValue().set(key, sell);
} else {
return "该商品已售罄";
}
return MessageFormat.format("该商品剩余余额:{0}", sell);
}
}
上述代码在并发情况下是会存在线程安全问题的,如果应用部署在单机可以考虑使用synchronized或者ReentrantLock进程锁是可以保证线程安全的,但是生成考虑高可用性,一般都是分布式,在分布式部署情况下,进程级别的锁是不能保证线程安全的,需要使用分布式锁来实现线程安全,redis的setnx命令就可以实现分布式锁。
分布式锁示例:
@RestController
public class OverSell {
@Autowired
private RedisTemplate redisTemplate;
@GetMapping("/overSell")
public String overSell(@RequestParam("key") String key) {
try {
Boolean absent = redisTemplate.opsForValue().setIfAbsent("lock." + key, "value");
if (absent) {
Integer sell = (Integer) redisTemplate.opsForValue().get(key);
sell = sell - 1;
if (sell > 0) {
redisTemplate.opsForValue().set(key, sell);
} else {
return "该商品已售罄";
}
return MessageFormat.format("该商品剩余余额:{0}", sell);
}
return "活动商品太火爆,请稍后再试";
} finally {
redisTemplate.delete("lock." + key);
}
}
}
上述代码中redisTemplate.opsForValue().setIfAbsen就是redis的setnx命令效果,如果不存在就设置成功,存在就返回false,分布式锁初步完成。上述示例在执行代码逻辑过程中,如果出现宕机finally快逻辑为被执行,那么key商品的代码逻辑永远都不会走到,分布式锁的key永远存在,基于这种情况,就需要为分布式锁加上一个过去时间,加锁和设置过期时间最好用一个命令设置,否则也会用原子性问题:
@RestController
public class OverSell {
@Autowired
private RedisTemplate redisTemplate;
@GetMapping("/overSell")
public String overSell(@RequestParam("key") String key) {
try {
Boolean absent = redisTemplate.opsForValue().setIfAbsent("lock." + key, "value", 30, TimeUnit.SECONDS);
if (absent) {
Integer sell = (Integer) redisTemplate.opsForValue().get(key);
sell = sell - 1;
if (sell > 0) {
redisTemplate.opsForValue().set(key, sell);
} else {
return "该商品已售罄";
}
return MessageFormat.format("该商品剩余余额:{0}", sell);
}
return "活动商品太火爆,请稍后再试";
} finally {
redisTemplate.delete("lock." + key);
}
}
}
这样为分布式锁增加了一个过期时间,这样就算程序宕机,分布式锁也会过期,不会一直存在;但是如果代码逻辑复杂,执行时间超过了设置的过期时间,那么其它线程就会获取到分布式锁,等待执行完逻辑代码后,当前线程又把其它线程的锁给删除了,由于代码逻辑执行超过了过期时间,使得每个线程都在删除其它线程加的分布式,这样导致分布式锁失效,导致超卖问题。可以考虑给锁加一个唯一id,再解锁的时候判断一下是否是自己加的锁,如果是自己加的锁才删除,示例代码:
@RestController
public class OverSell {
@Autowired
private RedisTemplate redisTemplate;
@GetMapping("/overSell")
public String overSell(@RequestParam("key") String key) {
String flag = UUID.randomUUID().toString();
try {
Boolean absent = redisTemplate.opsForValue().setIfAbsent("lock." + key, flag, 30, TimeUnit.SECONDS);
if (absent) {
Integer sell = (Integer) redisTemplate.opsForValue().get(key);
sell = sell - 1;
if (sell > 0) {
redisTemplate.opsForValue().set(key, sell);
} else {
return "该商品已售罄";
}
return MessageFormat.format("该商品剩余余额:{0}", sell);
}
return "活动商品太火爆,请稍后再试";
} finally {
if (flag.equals(redisTemplate.opsForValue().get("lock." + key))) {
redisTemplate.delete("lock." + key);
}
}
}
}
上述代码,如果并发量不是很高的情况下,可能不会有什么问题;如果并发量很高,代码刚判断好是自己加的锁时,锁的过期时间刚好到了,其它线程获取了分布式锁,那么又会出现删除不是自己的锁的情况,又会出现超卖问题;这个问题主要是因为过期时间导致的,在执行代码逻辑过程的时候,锁过期了,如果有另外一个线程,在主线执行代码逻辑的时候一直监听,如果逻辑没有执行,就不让锁过期,这种锁续命机制其实是有第三方框架实现好的,redisson框架
引入redisson依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.6.5</version>
</dependency>
需要将redisson的bean注入到springboot容器中
@Bean
public Redisson redisson() {
return (Redisson) Redisson.create(new Config(){{
useSingleServer().setAddress("redis://localhost:6379").setDatabase(0);
}});
}
redisson框架使用非常简单,逻辑大致是如果有一个线程获取到锁,执行代码逻辑,后台会每隔一段时间检查锁是否存在,如果存在给锁续命,其它线程如果没有获取到锁,会进行while循环,间隙性的获取锁,尝试一段时间后没有获取到锁会挂起不在自旋。代码逻辑如下
@RestController
public class OverSell {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private Redisson redisson;
@GetMapping("/overSell")
public String overSell(@RequestParam("key") String key) {
String flag = UUID.randomUUID().toString();
RLock lock = redisson.getLock("lock." + key);
lock.lock();
try {
Integer sell = (Integer) redisTemplate.opsForValue().get(key);
sell = sell - 1;
if (sell > 0) {
redisTemplate.opsForValue().set(key, sell);
} else {
return "该商品已售罄";
}
return MessageFormat.format("该商品剩余余额:{0}", sell);
} finally {
lock.unlock();
}
}
}
redisson分布式锁原理: