redis分布式锁使用

814 阅读3分钟

没有标题

小明在一家电商平台上班,开发下单业务。公司只有一台服务器,部署了一个服务,他的代码这么写的:

    synchronized (this){
        
        // 业务操作 :操作数据库减库存
    }

在多线程环境下,这段代码运行良好,没有问题。 后来,公司发展迅速,服务器变成了集群,部署了多个服务。之前的代码就会出现问题,因为synchronized,只能保证在一个jvm里是线程安全的。在多个jvm中,这段扣减库存会有安全问题。 小明想到分布式环境下可以使用reids来解决,他的代码这么写的:

    private void process(){

        String prodKey = "prod-001";
        try(Jedis jedis = getJedis()){

            Long setnx = jedis.setnx(prodKey, "1111");
            if(setnx == 0){
                // 没有设置成功,说明占用
                return;
            }

            try {
                // 业务操作 :操作数据库减库存
            } finally {
                jedis.del(prodKey);
            }

        }
    }

乍一看没问题,利用setnx,设置一个key,当他存在设置不成功,说明占用了,不存在设置成功,扣减库存,最后finally不忘把key删掉。
问题是假设某台服务器在程序执行到业务一半时挂了,此时这个锁依旧无法释放,造成死锁。
小明知道后,简单,再来一版如下:

    private void process(){

        String prodKey = "prod-001";
        try(Jedis jedis = getJedis()){

            Long setnx = jedis.setnx(prodKey, "1111");
            // 加上过期时间,自动失效
            jedis.expire(prodKey, 30);
            if(setnx == 0){
                // 没有设置成功,说明占用
                return;
            }

            try {
                // 业务操作 :操作数据库减库存
            } finally {
                jedis.del(prodKey);
            }

        }
    }

我加上失效时间不就行了,又有问题了;假设a线程执行业务超过30s了,锁释放,b线程过来加锁成功,a线程执行完执行了jedis.del(prodKey),结果把b线程的锁释放了,这是c线程过来,加锁成功,b执行完把c锁释放。。。。
小明知道后,简单,再来一版如下:

    private void process(){

        String prodKey = "prod-001";
        String clientId = getMachineNum() + UUID.randomUUID().toString();
        try(Jedis jedis = getJedis()){

            Long setnx = jedis.setnx(prodKey, clientId);

            if(setnx == 0){
                // 没有设置成功,说明占用
                return;
            }

            // 加上过期时间,自动失效
            jedis.expire(prodKey, 30);

            try {
                // 操作数据库减库存
            } finally {
                if(clientId.equals(jedis.get("prodKey"))){
                    // 只删除自己的key
                    jedis.del(prodKey);
                }
            }

        }
    }

我加了一个本机唯一标识clientId,在删除时判断一下是否一致,一致就删掉,这样就不会误删。
那之前那个还有一个问题,假设a线程执行业务超过了设定的过期时间怎么办?锁超时时间自动释放,此时还是和没锁一样,做了这么多没用的功。
小明知道后,简单,再来一版如下:

    private void process(){

        String prodKey = "prod-001";
        String clientId = getMachineNum() + UUID.randomUUID().toString();
        try(Jedis jedis = getJedis()){

            Long setnx = jedis.setnx(prodKey, clientId);

            if(setnx == 0){
                // 没有设置成功,说明占用
                return;
            }

            // 加上过期时间,自动失效
            jedis.expire(prodKey, 30);
            // 看门狗,给锁续命
            new Thread(new RenewalThread(jedis, prodKey)).start();

            try {
                // 操作数据库减库存
            } finally {
                if(clientId.equals(jedis.get("prodKey"))){
                    // 只删除自己的key
                    jedis.del(prodKey);
                }
            }

        }
    }

    class RenewalThread implements Runnable{

        Jedis jedis;
        String prodKey;
        boolean exist;

        public RenewalThread(Jedis jedis, String prodKey) {
            this.jedis = jedis;
            this.prodKey = prodKey;
            this.exist = true;
        }

        @Override
        public void run() {
            
            while(exist){
                if(jedis.exists(prodKey)){
                    jedis.expire(prodKey, 30);
                }else{
                    exist = false;
                }
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

现在大部分问题都解决了,但还有一个致命的问题;多个redis命令是非原子操作。在代码中有先setnx,再expire;有先查再删等。这些都有可能出现安全问题。在redis2.6版本新增了lua脚本解析。这个lua是支持多个命令执行,并且支持事务。我们只要在上面那些操作上换成lua脚本。至此reids分布式锁已有小成。
当你做完上面那些步骤的时候,你看下redisson,立马觉得redisson真香。
他帮我们把上面那些步骤都做好了,并且实现的更好。
使用redisson之后的上述代码:

    RedissonClient redissonClient = Redisson.create(new Config());
    
    String prodKey = "prod-001";
    // 获取锁
    RLock lock = redissonClient.getLock(prodKey);
    // 加锁
    lock.lock();
    
    try {
        // 操作数据库减库存
    } finally {
        // 解锁
        lock.unlock();
    }

实际代码只有3步,综上所述,大家去用redisson吧。