分布式锁实现

137 阅读2分钟

一,Redis实现分布式锁

实现redis分布式锁需要保证几个特性:

  • 互斥性。在任意时刻,只有一个客户端能持有锁。
  • 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
  • 有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
  • 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
1, 一眼就看出来的错误
* 获取锁
public boolean tryLock(String key,String testValue,int expireSeconds) {
        
        // 1,尝试获取redis锁, 获取成功直接返回
        if (redisTemplate.opsForValue().setIfAbsent(key, testValue)) {
            //若在这里程序突然崩溃,则无法设置过期时间,将发生死锁
            redisTemplate.expire(key, 5, TimeUnit.MINUTES);
        }
        ...
}
* 解锁
public static void wrongReleaseLock1(Jedis jedis, String lockKey) {
    //不先判断锁的拥有者而直接解锁的方式,会导致任何客户端都可以随时进行解锁,即使这把锁不是它的
    jedis.del(lockKey);
}
2, 网络上最容易混淆的错误之处2,很多博客都写错了
public boolean tryLock(String key,int expireSeconds){

        Date now = new Date();
        Date newExpire = DateUtils.addSeconds(now, expireSeconds);

        // 1,尝试获取redis锁, 获取成功直接返回
        if(redisTemplate.opsForValue().setIfAbsent(key,newExpire)){
            return true;
        }

        // 2,判断锁超时 - 防止原来的操作异常,没有运行解锁操作  防止死锁
        Date oldExpire = (Date) redisTemplate.opsForValue().get(key);
        if(oldExpire != null && now.compareTo(oldExpire) < 0){
            return false;
        }

        // 3,锁超时, 放入新的锁
        Date replacedExpire = (Date) redisTemplate.opsForValue().getAndSet(key,newExpire);
        //getAndSet 获取原来key键对应的值并重新赋新值,防止并发
        if (replacedExpire != null && replacedExpire.compareTo(oldExpire) == 0) {
            return true;
        } else {
            return false;
        }

    }
这种方式错误在哪里?
1. 由于是客户端自己生成过期时间,所以需要强制要求分布式下每个客户端的时间必须同步。 
2. 当锁过期的时候,在执行第3步的时候,如果多个客户端同时执行jedis.getSet()方法,那么虽然最终只有一个客户端可以加锁,但是这个客户端的锁的过期时间可能被其他客户端覆盖。
3. 锁不具备拥有者标识,即任何客户端都可以解锁。

* 解锁
public static void wrongReleaseLock2(Jedis jedis, String lockKey, String requestId) {

    // 判断加锁与解锁是不是同一个客户端
    if (requestId.equals(jedis.get(lockKey))) {
        // 若在此时,这把锁突然不是这个客户端的,则会误解锁
        jedis.del(lockKey);
    }
}
问题:jedis.del()方法的时候,这把锁已经不属于当前客户端的时候会解除他人加的锁。那么是否真的有这种场景?
    答案是肯定的,比如客户端A加锁,一段时间之后客户端A解锁,在执行jedis.del()之前,锁突然过期了或者锁过期时间短正常过期,
    此时客户端B尝试加锁成功,然后客户端A再执行del()方法,则将客户端B的锁给解除了。

那么正确的写法是什么呢? 采用redis+lua脚本实现,lua脚本有原子性的保障。 另外可以使用Redis官方提供的Java组件Redisson,如果你的项目中Redis是多机部署的,可以直接使用它。它的底层就是用lua脚本实现的。

3, redisson实现
public class LockTest {
    private static RedissonClient redissonClient;

    static {
        Config config=new Config();
        config.useSingleServer().setAddress("redis://***:6379");
        config.useSingleServer().setPassword("***");
        redissonClient= Redisson.create(config);
    }

    public static void main(String[] args) throws InterruptedException {

        ExecutorService executorService = Executors.newFixedThreadPool(5);

        for (int i = 0; i < 5 ; i++) {
            executorService.execute(new Runnable() {
                public void run() {
                    try {
                        RLock rLock=redissonClient.getLock("updateAccount_feng");
                        // 最多等待100s,超过100s就放弃等待;上锁10s以后过期自动释放锁
                        if(rLock.tryLock(100,10, TimeUnit.SECONDS)){
                            System.out.println(Thread.currentThread().getName() + "--获取锁成功");
                        }
                        Thread.sleep(2000);
                        rLock.unlock();
                        System.out.println(Thread.currentThread().getName() + "--释放锁成功");
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                }
            });
        }
    }

}

控制台输出----

redisson加锁和释放锁原理,在源码RedissonLock类中查看获取锁和释放锁的逻辑。

可以看出加锁和释放锁,为了保证redis的原子性,采用的是lua脚本实现的。

具体详情看下面的源码分析:Redis分布式锁redisson实现原理分析

2,Zookeeper实现分布式锁

public class MutexDemo {

    public static void main(String[] args) {

        CuratorFramework curatorFramework=
                CuratorFrameworkFactory.builder().
                        connectString("81.68.204.211:2181").
                        sessionTimeoutMs(5000).
                        retryPolicy(new ExponentialBackoffRetry
                                (1000,3)).
                        connectionTimeoutMs(4000).build();
        curatorFramework.start(); //表示启动.

        /**
         * locks 表示命名空间
         * 锁的获取逻辑是放在zookeeper
         * 当前锁是跨进程可见
         */
        InterProcessMutex lock=new InterProcessMutex(curatorFramework,"/locks");
        for(int i=0;i<10;i++){
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"->尝试抢占锁");
                try {
                    lock.acquire();//抢占锁,没有抢到,则阻塞
                    System.out.println(Thread.currentThread().getName()+"->获取锁成功");
                } catch (Exception e) {
                    e.printStackTrace();
                }
                try {
                    Thread.sleep(4000);
                    lock.release(); //释放锁
                    System.out.println(Thread.currentThread().getName()+"->释放锁成功");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            },"t-"+i).start();
        }
    }

}

每个线程都尝试在zk上建立了临时有序的节点,然后按照最小的节点编号依次获取锁进行逻辑处理,当释放锁后,会进行通知其他线程可以同时抢占锁了,然后剩下的最小序号的节点会获取锁,依次往下执行... 下面是不断刷新ZooInspector的图示: