1、导入redis包
org.springframework.boot
spring-boot-starter-data-redis
2.2.5.RELEASE
2、版本一,适合无并发情况
/**
* 版本一
* 读取redis剩余票
* 如果大于0,执行卖出
*/
@GetMapping("/one")
public void versionOne() {
Integer ticketLeft = Integer.valueOf(redisTemplate.opsForValue().get(TICKET).toString());
if (ticketLeft>0) {
ticketLeft = ticketLeft -1;
System.out.println("卖出第几张票:" + ticketLeft);
redisTemplate.opsForValue().set(TICKET,ticketLeft);
}
}
这个代码正常执行没有问题,但是如果出现高并发,会发生超卖现象,多个线程同一时刻取到了同样的剩余票数,用jmeter模拟高并发测试
image.png
image.png
发现高并发情况下,这种逻辑不适用,会出现一张票贩卖多次的情况
3、版本二,修改代码,适合并发情况
多个线程同时请求redis,通过setIfAbsent设置锁,相当于setnx,如果返回true,说明redis没有人设置过key,第一次跑 ,如果返回false,说明有人已经设置过了,正在执行代码,这时候直接给他返回,或者等待别的线程执行结束。
程序执行结束,一定要解除redis锁,给下个线程跑
/**
* 版本二
* 适合高并发情况下使用
*/
@GetMapping("/two")
public void test() {
// 给线程上锁,同一时刻只有一个线程可以从redis获取数据,true第一次上锁,false已有key,说明别人加过锁
Boolean lock = redisTemplate.opsForValue().setIfAbsent(LOCK_KEY, "lock");
if (lock) {
// 如果上锁成功,执行代码
Integer ticketLeft = Integer.valueOf(redisTemplate.opsForValue().get(TICKET).toString());
System.out.println("卖出第几张票:" + ticketLeft);
redisTemplate.opsForValue().set(TICKET,ticketLeft - 1+"");
} else {
System.out.println("获取锁失败");
}
// 买票结束,释放锁资源
redisTemplate.delete(LOCK_KEY);
}
使用jmeter并发测试,发现没有出现超卖情况
image.png
这段代码会有个问题,如果程序抛异常,那么最后的解锁步骤不会执行,会导致后面所有的线程全部获取锁失败,所以我们给锁加个过期时间
4、 版本三,防止代码异常,出现死锁
/**
* 版本三
* 防止死锁
*/
@GetMapping("/three")
public void three() {
// 给线程上锁,同一时刻只有一个线程可以从redis获取数据,true第一次上锁,false已有key,说明别人加过锁
Boolean lock = redisTemplate.opsForValue().setIfAbsent(LOCK_KEY, "lock");
redisTemplate.expire(LOCK_KEY,6, TimeUnit.SECONDS);
try {
if (lock) {
// 如果上锁成功,执行代码
Integer ticketLeft = Integer.valueOf(redisTemplate.opsForValue().get(TICKET).toString());
System.out.println("卖出第几张票:" + ticketLeft);
redisTemplate.opsForValue().set(TICKET,ticketLeft - 1+"");
} else {
System.out.println("获取锁失败");
}
}finally {
// 买票结束,释放锁资源
redisTemplate.delete(LOCK_KEY);
}
}
这个代码还有个问题,如果刚执行完上锁操作,服务器宕机,后面的过期时间没有设置,还是会出现死锁,所以需要保证上锁和设置过期时间同步执行,继续修改
5、 版本四,继续修改死锁情况
/**
* 版本四
* 防止死锁
*/
@GetMapping("/four")
public void four() {
// 给线程上锁,同一时刻只有一个线程可以从redis获取数据,true第一次上锁,false已有key,说明别人加过锁
Boolean lock = redisTemplate.opsForValue().setIfAbsent(LOCK_KEY, "lock",6,TimeUnit.SECONDS);
try {
if (lock) {
// 如果上锁成功,执行代码
Integer ticketLeft = Integer.valueOf(redisTemplate.opsForValue().get(TICKET).toString());
System.out.println("卖出第几张票:" + ticketLeft);
redisTemplate.opsForValue().set(TICKET,ticketLeft - 1+"");
} else {
System.out.println("获取锁失败");
}
}finally {
// 买票结束,释放锁资源
redisTemplate.delete(LOCK_KEY);
}
}
setIfAbsent有个方法,同时传入时间和单位,他会同步发送给redis,保证上锁和设置时间同步执行
Boolean setIfAbsent(K key, V value, long timeout, TimeUnit unit);
代码还有个问题,现在设置的过期时间是6s,假如有个线程1,业务代码需要执行10s,那么在执行到第7s的时候,锁失效,线程2获取到锁,开始执行业务代码,这时候会有线程1,线程2两段业务逻辑同步执行
继续执行到第10s的时候,线程1结束,执行解锁操作,删除key
但是,线程1的key已经失效过期,所以他删除的其实是线程2的key,这时候会导致线程3获取到锁执行代码,无限往复
继续修改代码
6、版本五,防止线程互删锁
解决思路,给每个线程一个不重复的随机数,解锁的时候先判断redis的锁是不是当初的锁,是的话,执行解锁
/**
* 版本5
* 防止线程互删锁
*/
@GetMapping("/five")
public void five() {
String value = UUID.randomUUID().toString();
// 给线程上锁,同一时刻只有一个线程可以从redis获取数据,true第一次上锁,false已有key,说明别人加过锁
Boolean lock = redisTemplate.opsForValue().setIfAbsent(LOCK_KEY, value,6,TimeUnit.SECONDS);
try {
if (lock) {
// 如果上锁成功,执行代码
Integer ticketLeft = Integer.valueOf(redisTemplate.opsForValue().get(TICKET).toString());
System.out.println("卖出第几张票:" + ticketLeft);
redisTemplate.opsForValue().set(TICKET,ticketLeft - 1+"");
} else {
System.out.println("获取锁失败");
}
}finally {
// 如果现在的锁还是当初上的锁,执行解锁
if (value.equals(redisTemplate.opsForValue().get(LOCK_KEY))){
// 买票结束,释放锁资源
redisTemplate.delete(LOCK_KEY);
}
}
}
继续优化代码,redis锁过期时间应该比程序运行时间长,但是程序运行时间不可控,所以加入守护线程给redis续期,只要程序在运行就一直给锁续期,知道程序结束
7、版本六,锁续期
/**
* 版本六,锁续期
*/
@GetMapping("/six")
public void six() {
String value = UUID.randomUUID().toString();
// 给线程上锁,同一时刻只有一个线程可以从redis获取数据,true第一次上锁,false已有key,说明别人加过锁
Boolean lock = redisTemplate.opsForValue().setIfAbsent(LOCK_KEY, value,10,TimeUnit.SECONDS);
try {
if (lock) {
// 执行续签
Thread thread = new Thread(new RedisDaemonTask(LOCK_KEY,value,redisTemplate));
thread.setDaemon(true);
thread.start();
Thread.sleep(30000);
// 如果上锁成功,执行代码
Integer ticketLeft = Integer.valueOf(redisTemplate.opsForValue().get(TICKET).toString());
System.out.println("卖出第几张票:" + ticketLeft);
redisTemplate.opsForValue().set(TICKET,ticketLeft - 1+"");
} else {
System.out.println("获取锁失败");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 如果现在的锁还是当初上的锁,执行解锁
if (value.equals(redisTemplate.opsForValue().get(LOCK_KEY))){
// 买票结束,释放锁资源
redisTemplate.delete(LOCK_KEY);
}
System.out.println("程序结束");
}
}
守护线程,如果锁没过期,并且还是当前进程的锁,就续期
public class RedisDaemonTask implements Runnable {
private String key;
private String value;
// 是否继续
private Boolean isContinue;
private RedisTemplate redisTemplate;
public RedisDaemonTask() {
}
public RedisDaemonTask(String key, String value, RedisTemplate redisTemplate) {
this.key = key;
this.value = value;
this.redisTemplate = redisTemplate;
this.isContinue = true;
}
@Override
public void run() {
while (isContinue) {
try {
// 原key过期时间
Long lock = redisTemplate.getExpire(key);
System.out.println("过期时间" + lock);
if (lock>0 && value.equals(redisTemplate.opsForValue().get(key))) {
// key还存在没有过期,手动续期
redisTemplate.expire("lock",lock + 5, TimeUnit.SECONDS);
System.out.println("续期成功");
} else {
// 如果没有获取到value,说明主线程解锁了,不继续续期
isContinue = false;
}
Thread.sleep(3000);
}catch (Exception e) {
e.printStackTrace();
}
}
}
}