如下代码段(在单机环境下多线程访问会有库存扣减不一致问题,比如线程一扣完库存,同时线程二争抢到资源,此时线程二获取到的库存仍是没有扣减的库存)
@RestController
public class IndexController{
@Autowired
private StringRedisTemplate stringRedisTempldate;
public String deductStock(){
int stock = Integer.parseInt(stringRedisTempldate.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";
}
}
单机环境下解决办法:加入同步块或者锁,修改如下:
@RestController
public class IndexController{
@Autowired
private StringRedisTemplate stringRedisTempldate;
public String deductStock(){
synchronized(this){
int stock = Integer.parseInt(stringRedisTempldate.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";
}
}
2.分布式环境下
分布式环境下上面的代码就会失效,仍然会出现库存扣减不一致的问题。假如我们通过nginx配置一个负载均衡,反响代理到自己模式的两台服务器上面,通过jmeter进行高并发请求压测,就会出现库存扣减不一致的问题,出现超卖的概率越大。synchronized可以控制解决单机的并发问题,不能跨jvm。
解决步骤思路如下:
1.使用用redis的setnx的命令(
sentnx命令格式:setnx key value
将key的值设为value,当且仅当key存在。
若给定的key已经存在,则setnx不做任何操作。
setnx是『set if not exists』(如果不存在,则set)的简写。
),可以简单的构建分布式锁。
@RestController
public class IndexController{
@Autowired
private StringRedisTemplate stringRedisTempldate;
public String deductStock(){
String lockKey = "kockKey";
Boolean result = stringRedisTempldate.opsForValue().setIfAbsent(lockKey,"abagg");
//true代表加锁成功,false代表加锁失败,直接返回错误
if(!result){
return "error_code";
}
int stock = Integer.parseInt(stringRedisTempldate.opsForValue().get("stock"));
if(stock > 0){
int realStock = stock - 1;
StringRedisTemplate.opsForValue().set("stock",realStock+"");
System.out.println("减扣成功,剩余库存:"+realStock );
}else{
System.out.println("减扣失败,余额不足");
}
stringRedisTempldate.delete(lockKey);
return "end";
}
}
原理大致如下:当多个请求同时进来访问nginx,反响代理到不同服务器上,当线程执行到redis上时,就会进行排队,通过setnx这条指令看看是否加锁成功,成功之后程序执行完,下个线程才能进来。
异常情况一:假如加锁成功,业务代码突然抛异常了,就意味着redis那边的key永远删除不了了,下面的请求再过来永远就获取不到锁,造成死锁。解决方法,加个try-finally,这样抛异常锁就正常删除,代码如下:
@RestController
public class IndexController{
@Autowired
private StringRedisTemplate stringRedisTempldate;
public String deductStock(){
String lockKey = "kockKey";
try{
Boolean result = stringRedisTempldate.opsForValue().setIfAbsent(lockKey,"abagg");
//true代表加锁成功,false代表加锁失败,直接返回错误
if(!result){
return "error_code";
}
int stock = Integer.parseInt(stringRedisTempldate.opsForValue().get("stock"));
if(stock > 0){
int realStock = stock - 1;
StringRedisTemplate.opsForValue().set("stock",realStock+"");
System.out.println("减扣成功,剩余库存:"+realStock );
}else{
System.out.println("减扣失败,余额不足");
}
}finally{
stringRedisTempldate.delete(lockKey);
}
return "end";
}
}
异常情况二:宕机。造成死锁。
解决方案:加个过期时间
@RestController
public class IndexController{
@Autowired
private StringRedisTemplate stringRedisTempldate;
public String deductStock(){
String lockKey = "kockKey";
try{
Boolean result = stringRedisTempldate.opsForValue().setIfAbsent(lockKey,"abagg");
StringRedisTemplate.expire(lockkey,10,TimeUnit.SECONDS);
//true代表加锁成功,false代表加锁失败,直接返回错误
if(!result){
return "error_code";
}
int stock = Integer.parseInt(stringRedisTempldate.opsForValue().get("stock"));
if(stock > 0){
int realStock = stock - 1;
StringRedisTemplate.opsForValue().set("stock",realStock+"");
System.out.println("减扣成功,剩余库存:"+realStock );
}else{
System.out.println("减扣失败,余额不足");
}
}finally{
stringRedisTempldate.delete(lockKey);
}
return "end";
}
}
情况三:但是这样仍然有原子性能问题,假如程序运行在设置过期时间之前,有的运维比较粗暴,直接kill -9这个进程,过期时间又会设置不成功,造成死锁。
解决方案:调用具有原子性的解决方法,将加锁与设置过期时间一步设置好。
@RestController
public class IndexController{
@Autowired
private StringRedisTemplate stringRedisTempldate;
public String deductStock(){
String lockKey = "kockKey";
try{
Boolean result = stringRedisTempldate.opsForValue().setIfAbsent(lockKey,"abagg",10,TimeUnit.SECONDS);
//true代表加锁成功,false代表加锁失败,直接返回错误
if(!result){
return "error_code";
}
int stock = Integer.parseInt(stringRedisTempldate.opsForValue().get("stock"));
if(stock > 0){
int realStock = stock - 1;
StringRedisTemplate.opsForValue().set("stock",realStock+"");
System.out.println("减扣成功,剩余库存:"+realStock );
}else{
System.out.println("减扣失败,余额不足");
}
}finally{
stringRedisTempldate.delete(lockKey);
}
return "end";
}
}
情况四:情况三的代码放到超高并发场景下还会出现问题,假如线程一执行整个程序需要15s,执行业务代码需要10s,此时redis锁失效,线程二此时就能获取到锁,假如线程二执行整个程序需要8s,执行业务代码需要5s,此时线程一在最终删除锁的时候就会删除的是线程二设置的锁,如此往下推,后台其他线程就会造成混乱,就会造成加的锁永久失效。
解决方案:在最终删除锁的时候做个判断,每一个线程进来设置一个唯一的id(生成方式根据公司具体方案定),放入redis中,在最终删除锁的时候判断一下redis中的id与当前的id是否同一个,是同一个再删除。
@RestController
public class IndexController{
@Autowired
private StringRedisTemplate stringRedisTempldate;
public String deductStock(){
String lockKey = "kockKey";
String clinetId = UUID.randomUUID().toString();
try{
Boolean result = stringRedisTempldate.opsForValue().setIfAbsent(lockKey,clientId,10,TimeUnit.SECONDS);
//true代表加锁成功,false代表加锁失败,直接返回错误
if(!result){
return "error_code";
}
int stock = Integer.parseInt(stringRedisTempldate.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(stringRedisTempldate.opsForValue.get(lockKey ))){
stringRedisTempldate.delete(lockKey);
}
}
return "end";
}
}
情况五:情况四执行的代码还是有点小问题,假如线程一执行的代码超过redis的锁超时时间,线程二又可以进来获取到锁,又会存在同时执行业务代码的问题,又会出现超卖。



