声明:本站文章均为作者个人原创,图片均为实际截图。如有需要请收藏网站,禁止转载,谢谢配合!!!

1、案例一

总库存:50
不上锁
会出现超卖现象

jmeter: 100个请求 1s内执行完毕

代码如下

下载附件

jmeter

下载附件

结果

下载附件

机器性能较好的情况下,会发现出现超卖情况,100个用户去抢购50个库存,结果还有剩余,这显然是不理想的。
因此需要改进。

2、案例二

总库存:50
加单体锁
不会出现超卖现象

jmeter: 100个请求 1s内执行完毕

代码

public class HomeController {

    @Autowired
    RedisTemplate<String, String> redisTemplate;

    @RequestMapping("/test")
    public void abc(){
        //加单体锁
        synchronized (this){
            Integer stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));

            if (stock > 0){
                int afterStock = stock - 1;
                redisTemplate.opsForValue().set("stock", Integer.toString(afterStock));
                System.out.println("【八点博客】恭喜您抢购成功,剩余库存数量:" + afterStock);
            }else{
                System.out.println("【八点博客】库存不足,抢购失败");
            }
        }
    }
}

下载附件

结果

下载附件

可以看到,如果代码只部署在一台机器上,则加上锁就能防止超卖。但是如果是项目集群部署,则此方法就无效了,因为单体锁只能在同一个jvm中生效。

3、案例三

总库存:50
分别在8080 8081端口模拟2台服务器部署项目,在使用nginx负载均衡 7000 -> 8080 8081
加单体锁
会出现超卖现象

jmeter: 100个请求 1s内执行完毕

nginx 配置

下载附件

代码 【和案例二一样】

public class HomeController {

    @Autowired
    RedisTemplate<String, String> redisTemplate;

    @RequestMapping("/test")
    public void abc(){
        //加单体锁
        synchronized (this){
            Integer stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));

            if (stock > 0){
                int afterStock = stock - 1;
                redisTemplate.opsForValue().set("stock", Integer.toString(afterStock));
                System.out.println("【八点博客】恭喜您抢购成功,剩余库存数量:" + afterStock);
            }else{
                System.out.println("【八点博客】库存不足,抢购失败");
            }
        }
    }
}

下载附件

结果如下

8080服务器

下载附件

8081服务器

下载附件

可以看到,如果代码部署在多台机器上,则加上锁依然不能防止超卖。

综上所述,对于分布式部署的项目来说,普通的java程序代码加锁无法解决问题,因为代码部署在不同的地方,不可控。
那么就可以转变思路,是否可以在相同资源redis上面加锁呢,因此引入案例4

4、案例四

由于命令SETNX:向Redis中添加一个key,只用当key不存在的时候才添加并返回1,存在则不添加返回0。并且这个命令是原子性的。因此我们可以利用这个特效,进行加锁。

@RestController
public class HomeController {

    @Autowired
    RedisTemplate<String, String> redisTemplate;

    @RequestMapping("/test")
    public void abc(){
        //如果不存在这个key,则添加一个key,这个操作是原子性的
        Boolean isGetRedisLock = redisTemplate.opsForValue().setIfAbsent("redisLock", "badianboke");
        if (isGetRedisLock){
            Integer stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));

            if (stock > 0){
                int afterStock = stock - 1;
                redisTemplate.opsForValue().set("stock", Integer.toString(afterStock));
                System.out.println("【八点博客】恭喜您抢购成功,剩余库存数量:" + afterStock);
            }else{
                System.out.println("【八点博客】库存不足,抢购失败");
            }

            redisTemplate.delete("redisLock"); //执行完毕,则删除key,便于后续进程访问
        }else{
            abc(); //获取不到锁,则自旋
        }


    }
}

下载附件

此时,进行多台服务器部署压测,则发现已经解决了超卖问题。

但是此时又有一个问题,如果业务代码在执行中出错或者宕机,则这个key永远不会过期,那么后续进程就无法进入到此业务中,就会导致死锁问题。因此需要保证即使在业务代码执行出错的情况下,这个key也能到期或者被释放掉

5、案例五

可以设置10s后自动过期

public class HomeController {

    @Autowired
    RedisTemplate<String, String> redisTemplate;

    @RequestMapping("/test")
    public void abc(){
        //如果不存在这个key,则添加一个key,这个操作是原子性的
        Boolean isGetRedisLock = redisTemplate.opsForValue().setIfAbsent("redisLock", "badianboke");
        if (isGetRedisLock){

            //10s后自动过期
            redisTemplate.expire("redisLock", 10, TimeUnit.SECONDS);

            Integer stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));

            if (stock > 0){
                int afterStock = stock - 1;
                redisTemplate.opsForValue().set("stock", Integer.toString(afterStock));
                System.out.println("【八点博客】恭喜您抢购成功,剩余库存数量:" + afterStock);
            }else{
                System.out.println("【八点博客】库存不足,抢购失败");
            }

            redisTemplate.delete("redisLock"); //执行完毕,则删除key,便于后续进程访问
        }else{
            abc(); //获取不到锁,则自旋
        }


    }
}

此方法存在一个问题,就是比如一个线程刚设置完key,服务器就宕机了,那么这个key永远无法释放,依然存在死锁问题。
因此需要把设置key、设置过期时间合并为一个原子操作。

6、案例六
设置key、10s后自动过期【原子操作】

@RestController
public class HomeController {

    @Autowired
    RedisTemplate<String, String> redisTemplate;

    @RequestMapping("/test")
    public void abc(){
        //设置key、10s后自动过期【原子操作】
        Boolean isGetRedisLock = redisTemplate.opsForValue().setIfAbsent("redisLock", "badianboke", 10, TimeUnit.SECONDS);
        if (isGetRedisLock){


            Integer stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));

            if (stock > 0){
                int afterStock = stock - 1;
                redisTemplate.opsForValue().set("stock", Integer.toString(afterStock));
                System.out.println("【八点博客】恭喜您抢购成功,剩余库存数量:" + afterStock);
            }else{
                System.out.println("【八点博客】库存不足,抢购失败");
            }

            redisTemplate.delete("redisLock"); //执行完毕,则删除key,便于后续进程访问
        }else{
            abc(); //获取不到锁,则自旋
        }


    }
}

但是又产生了两个新的问题,如果在 0 时刻,设置了到第10s会自动到期,如果业务代码一直到第20s才执行完。

问题1:10s时候key过期。则B线程会进来,而A线程业务代码还在执行,这样就会导致锁失效、锁不住的问题。
【解决方法】可以考虑把key自动续签,比如使用redisson

问题2:20s时候A业务代码已经执行完毕,就会执行删除key的操作,而此时的线程属于B的,等于直接把B的key删了,这会导致其他线程的锁被删。
【解决方法】可以用当前线程标记当前线程加的锁,比如使用 uuid,每个线程的uuid都不一样,删除之前判断一下,当前锁是否属于当前线程即可,若属于则删,不属于则不做操作

7、案例七

解决案例六中问题2:删别人锁

public class HomeController {

    @Autowired
    RedisTemplate<String, String> redisTemplate;

    @RequestMapping("/test")
    public void abc(){
        //设置key、10s后自动过期【原子操作】


        String badianbokeUUID = UUID.randomUUID().toString();

        Boolean isGetRedisLock = redisTemplate.opsForValue().setIfAbsent("redisLock", badianbokeUUID, 10, TimeUnit.SECONDS);
        if (isGetRedisLock){


            Integer stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));

            if (stock > 0){
                int afterStock = stock - 1;
                redisTemplate.opsForValue().set("stock", Integer.toString(afterStock));
                System.out.println("【八点博客】恭喜您抢购成功,剩余库存数量:" + afterStock);
            }else{
                System.out.println("【八点博客】库存不足,抢购失败");
            }

            //属于该进程才删除key,放置误删其他进程的key
            if (redisTemplate.opsForValue().get("redisLock").equals(badianbokeUUID)){
                redisTemplate.delete("redisLock"); //执行完毕,则删除key,便于后续进程访问
            }
        }else{
            abc(); //获取不到锁,则自旋
        }


    }
}

这样做依然会存在一个问题,就是

if (redisTemplate.opsForValue().get("redisLock").equals(badianbokeUUID)){
   redisTemplate.delete("redisLock"); //执行完毕,则删除key,便于后续进程访问
}

取值-比值-删除key 这个过程并非原子性的,比如A的key值0001,当A取到key为0001时候,B同时把key改为了0002,但是A依然会判定相等,把b的key删掉

因此,需要保证这三步原子性。可以使用lua表达式

8、案例八

使用lua表达式解决案例七中取值-比值-删除key无法原子性操作问题

@RestController
public class HomeController {

    @Autowired
    RedisTemplate<String, String> redisTemplate;

    @RequestMapping("/test")
    public void abc(){
        //设置key、10s后自动过期【原子操作】
        String badianbokeUUID = UUID.randomUUID().toString();

        Boolean isGetRedisLock = redisTemplate.opsForValue().setIfAbsent("redisLock", badianbokeUUID, 10, TimeUnit.SECONDS);
        if (isGetRedisLock){
            Integer stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));

            if (stock > 0){
                int afterStock = stock - 1;
                redisTemplate.opsForValue().set("stock", Integer.toString(afterStock));
                System.out.println("【八点博客】恭喜您抢购成功,剩余库存数量:" + afterStock);
            }else{
                System.out.println("【八点博客】库存不足,抢购失败");
            }
            //原子操作
            String luaCommand = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

            redisTemplate.execute(new DefaultRedisScript<>(luaCommand, Long.class), Arrays.asList("redisLock"), badianbokeUUID);

        }else{
            abc(); //获取不到锁,则自旋
        }
    }
}