redis分布式锁实战

单机架构场景解决并发问题

1: 无锁版商品购买

public boolean snatch(int goodsId){

        //获取商品库存
        String stockStr = stringRedisTemplate.opsForValue().get(String.valueOf(goodsId));
        int stock = Integer.parseInt(stockStr);

        if(stock > 0){
            System.out.println("buy success, stock :" + stock--);
            stringRedisTemplate.opsForValue().set(String.valueOf(goodsId),String.valueOf(stock));
        }else{
            System.out.println("stock is zero");
            return false;
        }

        return true;
}

问题 : 当大量用户抢购时候,非常容易造成某个商品的重复出售问题; 不会有超卖问题;redis是单线程的; 

2: Lock加锁版本商品

public boolean snatch1(int goodsId){

        Lock lock = new ReentrantLock();
        //获取商品库存
        String stockStr = stringRedisTemplate.opsForValue().get(String.valueOf(goodsId));
        int stock = Integer.parseInt(stockStr);

        lock.lock();
        if(stock > 0){
            System.out.println("buy success, stock :" + stock--);
            stringRedisTemplate.opsForValue().set(String.valueOf(goodsId),String.valueOf(stock));
        }else{
            System.out.println("stock is zero");
            return false;
        }
        lock.unlock();
        return true;
}

问题 : 没锁住获取库存的代码,导致很多用户同样会出现问题

3: synchronized加锁

public synchronized boolean snatch2(int goodsId){

        //获取商品库存
        String stockStr = stringRedisTemplate.opsForValue().get(String.valueOf(goodsId));
        int stock = Integer.parseInt(stockStr);

        if(stock > 0){
            System.out.println("buy success, stock :" + stock--);
            stringRedisTemplate.opsForValue().set(String.valueOf(goodsId),String.valueOf(stock));
        }else{
            System.out.println("stock is zero");
            return false;
        }
        return true;
}

问题 : 锁住了整个方法,但是导致单体下不适用抢购代码

4: nginx搭建演示

upstream redis{ 
server 192.168.25.111:8080; 
server 192.168.25.111:8081; 
server 192.168.25.111:8082; 

location /dxx 

    proxy_pass http://redis/; 
}

 

集群架构场景解决并发问题

1: 基于SETNX锁

https://redis.io/commands/setnx/

public boolean snatch3(int goodsId){

        //redis的setnx命令;如果存在这个值,就不会再设置了;
        Boolean locked = stringRedisTemplate.opsForValue().setIfAbsent("lock", "ldx");

        if(!locked){ //没拿到锁,代表获取商品失败
            return false;
        }

        //获取商品库存
        String stockStr = stringRedisTemplate.opsForValue().get(String.valueOf(goodsId));
        int stock = Integer.parseInt(stockStr);

        if(stock > 0){
            System.out.println("buy success, stock :" + stock--);
            stringRedisTemplate.opsForValue().set(String.valueOf(goodsId),String.valueOf(stock));
        }else{
            System.out.println("stock is zero");
            return false;
        }

        //解锁
        stringRedisTemplate.delete("lock");

        return true;
    }

问题 : 有死锁bug,当代码出现异常,不会进行解锁

2: try解决死锁BUG

public boolean snatch4(int goodsId){

        //redis的setnx命令;如果存在这个值,就不会再设置了;
        Boolean locked = stringRedisTemplate.opsForValue().setIfAbsent("lock", "ldx");

        if(!locked){ //没拿到锁,代表获取商品失败
            return false;
        }

        try{
            //获取商品库存
            String stockStr = stringRedisTemplate.opsForValue().get(String.valueOf(goodsId));
            int stock = Integer.parseInt(stockStr);

            if(stock > 0){
                System.out.println("buy success, stock :" + stock--);
                stringRedisTemplate.opsForValue().set(String.valueOf(goodsId),String.valueOf(stock));
            }else{
                System.out.println("stock is zero");
                return false;
            }
            return true;
        }finally {
            stringRedisTemplate.delete("lock");
        }
}

问题 : 多个商品之间,拥有同样的锁;如果是不同商品,会把其他人的锁给解掉

3: 多商品锁BUG

public boolean snatch5(int goodsId){

        //每个商品都是自己单独的锁
        Boolean locked = stringRedisTemplate.opsForValue().setIfAbsent(String.valueOf(goodsId), "ldx");

        if(!locked){ //没拿到锁,代表获取商品失败
            return false;
        }

        try{
            //获取商品库存
            String stockStr = stringRedisTemplate.opsForValue().get(goodsId+"stock");
            int stock = Integer.parseInt(stockStr);

            if(stock > 0){
                System.out.println("buy success, stock :" + stock--);
                stringRedisTemplate.opsForValue().set(goodsId+"stock",String.valueOf(stock));
            }else{
                System.out.println("stock is zero");
                return false;
            }
            return true;
        }finally {
            //释放锁
            stringRedisTemplate.delete(String.valueOf(goodsId));
        }
    }

问题 : 如果程序执行时,加上锁.这时候有一台机器重启了,导致代码没有解锁

4: 添加超时机制,解决被锁问题

public boolean snatch6(int goodsId){

        //设置超时时间,指定5秒释放锁;
        //Boolean locked = stringRedisTemplate.opsForValue().setIfAbsent(String.valueOf(goodsId), "ldx");
        //stringRedisTemplate.expire(String.valueOf(goodsId),5, TimeUnit.SECONDS);

        //保证原子性
        Boolean locked = stringRedisTemplate.opsForValue().setIfAbsent(String.valueOf(goodsId), "ldx",5, TimeUnit.SECONDS);

        if(!locked){ //没拿到锁,代表获取商品失败
            return false;
        }

        try{
            //获取商品库存
            String stockStr = stringRedisTemplate.opsForValue().get(String.valueOf(goodsId));
            int stock = Integer.parseInt(stockStr);

            if(stock > 0){
                System.out.println("buy success, stock :" + stock--);
                stringRedisTemplate.opsForValue().set(String.valueOf(goodsId),String.valueOf(stock));
            }else{
                System.out.println("stock is zero");
                return false;
            }
            return true;
        }finally {
            //释放锁
            stringRedisTemplate.delete(String.valueOf(goodsId));
        }
    }

一般的场景下,这样的代码已经可以使用了.基本上很难复现问题,就算有问题,也直接修改数据库排查就好了.但是在真实的互联网大并发的场景下,这个肯定是不行的.因为他们可以达到1s生成1万甚至10万订单的情况.

问题 : 多个请求依次来进行处理,其中某一个请求处理时间需要 10s时间, 第二个请求需要8s时间,第三个请求需要4s时间;但是当前锁的超时时间是5s;可能会导致循环交替解锁,进而引发锁失效的逻辑BUG.

5: 解决锁失效问题

public boolean snatch7(int goodsId){

        //设置超时时间,指定5秒释放锁;
        //Boolean locked = stringRedisTemplate.opsForValue().setIfAbsent(String.valueOf(goodsId), "ldx");
        //stringRedisTemplate.expire(String.valueOf(goodsId),5, TimeUnit.SECONDS);

        //定义一个随机UUID
        String lockValue = "lockldx" + System.currentTimeMillis() + Math.random() + goodsId;

        //保证原子性
        Boolean locked = stringRedisTemplate.opsForValue().setIfAbsent(String.valueOf(goodsId), lockValue,5, TimeUnit.SECONDS);

        if(!locked){ //没拿到锁,代表获取商品失败
            return false;
        }

        try{
            //获取商品库存
            String stockStr = stringRedisTemplate.opsForValue().get(goodsId+"stock");
            int stock = Integer.parseInt(stockStr);

            if(stock > 0){
                System.out.println("buy success, stock :" + stock--);
                stringRedisTemplate.opsForValue().set(goodsId+"stock",String.valueOf(stock));
            }else{
                System.out.println("stock is zero");
                return false;
            }
            return true;
        }finally {
            //释放锁
            if(lockValue.equals(stringRedisTemplate.opsForValue().get(String.valueOf(goodsId)))){
                stringRedisTemplate.delete(String.valueOf(goodsId));
            }
        }
    }

问题 : 这样解决了自己释放自己的锁的问题,但是没有本质上解决这个超时时间控制的问题

6: 刷新超时时间

public boolean snatch8(int goodsId){

        //设置超时时间,指定5秒释放锁;
        //Boolean locked = stringRedisTemplate.opsForValue().setIfAbsent(String.valueOf(goodsId), "ldx");
        //stringRedisTemplate.expire(String.valueOf(goodsId),5, TimeUnit.SECONDS);

        //定义一个随机UUID
        String lockValue = null;
        try {
            lockValue = "lockldx" + System.currentTimeMillis() + Math.random() + InetAddress.getLocalHost().getHostAddress();
        } catch (UnknownHostException e) {
            lockValue = UUID.randomUUID().toString();
        }

        int locktime = 5;//超时时间,单位s

        //保证原子性
        Boolean locked = stringRedisTemplate.opsForValue().setIfAbsent(String.valueOf(goodsId), lockValue,locktime, TimeUnit.SECONDS);

        if(!locked){ //没拿到锁,代表获取商品失败
            return false;
        }

        String lockValue2 = lockValue;

        try{
            long now = System.currentTimeMillis();//获取锁的时间
            //开启线程,去对过期时间实时扫描
            Thread th = new Thread(){
                public void run() {

                    while (true){

                        //线程是否中断
                        if(Thread.currentThread().isInterrupted()){
                            break;
                        }

                        //如果获取到了,代表已经超时了
                        stringRedisTemplate.opsForValue().setIfAbsent(String.valueOf(goodsId), lockValue2, locktime/2, TimeUnit.SECONDS);
                    }
                }
            };

            //获取商品库存
            String stockStr = stringRedisTemplate.opsForValue().get(goodsId+"stock");
            int stock = Integer.parseInt(stockStr);

            if(stock > 0){
                System.out.println("buy success, stock :" + stock--);
                stringRedisTemplate.opsForValue().set(goodsId+"stock",String.valueOf(stock));
                //th.stop();
                th.interrupt();
            }else{
                System.out.println("stock is zero");
                //th.stop();
                th.interrupt();
                return false;
            }
            return true;
        }finally {
            //释放锁
            if(lockValue.equals(stringRedisTemplate.opsForValue().get(String.valueOf(goodsId)))){
                stringRedisTemplate.delete(String.valueOf(goodsId));
            }
        }
    }

问题 : 这种方案初步看起来没问题,但是经不起推敲,因为线程的不可控以及其他的处理.所以我们使用开源组件Redisson成熟的方案

7: 基于Redisson实现

public boolean snatch8(int goodsId){

        //定义一个随机UUID
        String lockValue = null;
        try {
            lockValue = "lockldx" + System.currentTimeMillis() + Math.random() + InetAddress.getLocalHost().getHostAddress();
        } catch (UnknownHostException e) {
            lockValue = UUID.randomUUID().toString();
        }

        int locktime = 5;//超时时间,单位s
        RLock lock = redissonClient.getLock(lockValue);
        lock.lock(locktime,TimeUnit.SECONDS);

        //lock.tryLock(locktime,TimeUnit.SECONDS);//不阻塞

        try{
            //获取商品库存
            String stockStr = stringRedisTemplate.opsForValue().get(goodsId+"stock");
            int stock = Integer.parseInt(stockStr);

            if(stock > 0){
                System.out.println("buy success, stock :" + stock--);
                stringRedisTemplate.opsForValue().set(goodsId+"stock",String.valueOf(stock));

            }else{
                System.out.println("stock is zero");

                return false;
            }
            return true;
        }finally {
            //释放锁
            lock.unlock();
        }
    }

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值