redis分布式锁探讨

 

基于jedis的能力,探讨了分布式锁的一些初级实现

public class DistributeLock {

    private JedisCluster jedisCluster;

    public DistributeLock() {
        Set<HostAndPort> hostAndPorts = new HashSet<>();
        hostAndPorts.add(new HostAndPort("xx.xx.xx.xx", 6379));
        jedisCluster = new JedisCluster(hostAndPorts, null, "password");
    }

    /*
    方案1:setnx + expire

    存在问题:
    1、setnx和expire操作两步不是原子操作,如果两步之间服务挂了,或者机器宕机,那么锁将永远无法获取
    2、若超时时间内(30s),业务(38s)无法执行完。锁被释放后,被其他线程获取,业务会出现并发问题
     */
    boolean getDistributeLock_1(String lockName) {
        long result = jedisCluster.setnx(lockName, "1");
        if (result == 1) {
            // 加锁成功
            jedisCluster.expire(lockName, 30);
            return true;
        }
        // 加锁失败
        return false;
    }

    /*
    方案2:lua脚本合并 setnx + expire

    解决问题:
    1、通过lua脚本将setnx和expire实现为原子操作
    存在问题:
    1、若超时时间内(30s),业务(38s)无法执行完。锁被释放后,被其他线程获取,业务会出现并发问题
     */
    boolean getDistributeLock_2(String lockName, String value) {
        String lusScript = "local result1 = redis.call('setnx', KEYS[1], ARGV[2])\n" +
                "if result1 == 1 then\n" +
                "redis.call('expire', KEYS[1], ARGV[1])\n" +
                "return 1\n" +
                "else return 0\n" +
                "end;";
        Long result = (Long) jedisCluster.eval(lusScript, Arrays.asList(lockName), Arrays.asList("30", value));
        return result == 1L;
    }

    /*
    以上两个方案,都是基于并发量不高的场景,比如分布式的任务。也并没有提供解锁的相关操作,
    1、如果锁的ttl设置长了,那其他抢锁的线程就会等待
    2、如果锁的ttl设置短了,可能会有并发问题
     */

    /*
    方案3:lua(setnx + expire) + del锁

    解决问题:
    1、线程执行完了,及时释放锁
    存在问题:
    1、若超时时间内(30s),业务(38s)无法执行完。锁被释放后,被其他线程获取,业务会出现并发问题
    2、在问题1的基础上,比如线程a先拿到锁,业务实际需要执行35s。30s后ttl失效,线程b抢到锁,5s后,线程a删除锁,a把b的锁删掉了。然后线程c抢到锁,业务混乱,锁失效
     */
    void doBusiness_3() {
        String lockName = "wzl_lock";
        // 抢锁
        boolean result = getDistributeLock_2(lockName, "1");

        if (!result) {
            return;
        }

        try {
            // 业务逻辑
            // 1、解放宝岛
            // 2、宝岛炒房
            // ...

        } finally {
            // 解锁
            jedisCluster.del(lockName);
        }
    }

    /*
    方案4:lua(setnx + expire) + del锁前判断是否是自己的锁

    解决问题:
    1、解决方案3的问题2(删除别人的锁)
    存在问题:
    1、若超时时间内(30s),业务(38s)无法执行完。锁被释放后,被其他线程获取,业务会出现并发问题
    2、解锁获取和删除两步是非原子操作
     */
    void doBusiness_4() {
        String lockName = "wzl_lock";
        String uuid = UUID.randomUUID().toString();
        // 抢锁
        boolean result = getDistributeLock_2(lockName, uuid);

        if (!result) {
            return;
        }

        try {
            // 业务逻辑
            // 1、解放宝岛
            // 2、宝岛炒房
            // ...

        } finally {
            // 解锁
            if (uuid.equals(jedisCluster.get(lockName))) {
                jedisCluster.del(lockName);
            }
        }
    }

    /*
    方案5:lua(setnx + expire) + lua(get + del)

    解决问题:
    1、解决方案4的问题2
    存在问题:
    1、若超时时间内(30s),业务(38s)无法执行完。锁被释放后,被其他线程获取,业务会出现并发问题
     */
    void doBusiness_5() {
        String lockName = "wzl_lock";
        String uuid = UUID.randomUUID().toString();
        // 抢锁
        boolean result = getDistributeLock_2(lockName, uuid);

        if (!result) {
            return;
        }

        try {
            // 业务逻辑
            // 1、解放宝岛
            // 2、宝岛炒房
            // ...

        } finally {
            // 解锁
            getAndDel(lockName, uuid);
        }
    }

    boolean getAndDel(String key, String value) {
        String lusScript = "local result1 = redis.call('get', KEYS[1])\n" +
                "if result1 == ARGV[1] then\n" +
                "redis.call('del', KEYS[1])\n" +
                "return 1\n" +
                "else return 0\n" +
                "end;";
        Long result = (Long) jedisCluster.eval(lusScript, Arrays.asList(key), Arrays.asList(value));
        return result == 1L;
    }

    public static void main(String[] args) {
        DistributeLock distributeLock = new DistributeLock();
        distributeLock.doBusiness_5();
    }
}

 

以上实现都不完善,即使是方案5,还是没有解决锁的TTL小于业务真实执行时间,导致的并发问题。

业界对于redis分布式锁的实现,目前所了解的还要数Redisson的实现。

对于方案5的遗留问题,可以通过启动一个看门狗线程,每隔一段时间,查看下锁是否过期,如果没有过期,就重置锁的ttl。

 

public class RedissonDistributeLock {

    private RedissonClient redissonClient;

    public RedissonDistributeLock() {
        Config config  =new Config();
        config.useSingleServer().setAddress("redis://xx.xx.xx.xx:6379");
        config.useSingleServer().setPassword("password");

        redissonClient = Redisson.create(config);
    }

    public void doBusiness_6() {
        String lockName = "wzl_lock";

        RLock lock = redissonClient.getLock(lockName);

        try {
            // 加锁
            lock.lock();
            // 业务逻辑
            // 1、解放世界无产阶级
            // 2、无产阶级万岁
            // ...
        } finally {
            // 解锁
            lock.unlock();
        }
    }
}

 

遗留:redisson分布式锁源码分析

posted @ 2022-09-29 16:19  _wzl  阅读(45)  评论(0编辑  收藏  举报