使用Redis实现分布式锁

文章目录

    • 1.Redis分布式锁原理
    • 2.Redis分布式锁代码实现
        • 1.1 分布式锁第一版
        • 1.2 分布式锁第二版
        • 1.3 分布式锁第三版
    • 3. 测试代码

在分布式系统中,操作不同系统或者同系统不同主机的共享资源时,为了保持一致性,就需要用到分布式锁。分布式锁有很多种实现方方法,比如zookeeper,redis等等。作为分布式系统的指挥官,用zookeeper实现分布式锁是很合适的,但本文讨论用Redis的setnx指令特性实现分布式锁。Redis的应用场景非常广泛,这种分布式锁的实现方式也非常的常用。

1.Redis分布式锁原理

使用Redis实现分布式锁,就不得不先了解一个指令,setnx key value。这个指令的功能是set 之前先检查redis中师傅存在该key,若不存在,则set key value,返回1;若存在,则什么都不做,返回0。
多个setnx请求同时发到redis,只有一个请求会返回1。根据这个特性,可以实现分布式锁。比如多个线程同时像操作id为1的订单,在操作redis该订单数据前,先向redis发送setnx order:1:lock value指令,value最好为一个唯一标识。这时只有一个线程返回1,set成功。这时我们认为这个线程抢到了锁。可以操作订单数据,操作完成之后删掉order:1:lock这个key。
下图显示分布式锁流程:
使用Redis实现分布式锁_第1张图片
下面用jedis代码实现一个简单的分布式锁:

2.Redis分布式锁代码实现

1.1 分布式锁第一版

先定义分布式锁接口,定义两个简单的加锁和解锁的 方法
DistributeLock

public interface DistributeLock {
    boolean lock(String lockKey,String lockvalue,int expire);
    boolean unlock(String lockKey,String lockvalue);
}

然后写它的实现:

RedisLock

public class RedisLock implements DistributeLock  {
    private Jedis jedis;

    public RedisLock() {}

    public RedisLock(Jedis jedis) {
        this.jedis = jedis;
    }

    @Override
    public boolean lock(String lockKey, String lockvalue, int expire) {
        Long setnx = jedis.setnx(lockKey, lockvalue);
        if (1L == setnx){
            jedis.expire(lockKey, expire);
            return true;
        }
        return false;
    }

    @Override
    public boolean unlock(String lockKey,String lockvalue) {
        return jedis.del(lockKey) == 1L;
    }
}

定义lock方法的时候,在参数中加了expire超时时间。这是为了防止如果del失败,lockKey一直存在于redis中,其他线程无法获取到锁。
这个lockKey需要能表示要加锁的数据,比如具体某一个订单。
原则上我们对lockValue值没有要求,但是一般我们会设置一个唯一标识作为value。比如线程号,或者时间戳等。
这一版代码非常简单,也有一定的问题。如果加锁过程中setnx成功,expire指令由于某种原因,比如redis挂掉,没能执行,那么lockKey就不会过期,可能永久留在redis里,造成死锁。所以这里需要保证setnx和expire指令的原子性。

1.2 分布式锁第二版

保证指令的原子性的方式有多种,redis事务机制,lua脚本,以及redis的set key value [EX seconds] [PX milliseconds] [NX|XX]指令。其中,后两种方式比较简单也常用。而redis的事务机制非常的不友好,存在一定缺陷,尽量不要使用。

因为redis支持lua脚本,并且可以将脚本缓存起来重复使用,性能还不错。lua脚本会包含多条指令,随着脚本要么都执行,要么都不执行。下面先看看lua脚本的方式保证set和expire指令的原子性。

public class RedisLock2 implements DistributeLock {
    private Jedis jedis;
    public static final Long SUCCESS_CODE_1 = 1L;

    public RedisLock2() {}

    public RedisLock2(Jedis jedis) {
        this.jedis = jedis;
    }

    @Override
    public boolean lock(String lockKey, String lockvalue, int expire) {
        String scrip = "if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then return redis.call('expire',KEYS[1], ARGV[2]) end return 0";
        ArrayList<String> params = new ArrayList<String>();
        params.add(lockvalue);
        params.add(String.valueOf(expire));
        Object eval = jedis.eval(scrip, Collections.singletonList(lockKey), params);
        return SUCCESS_CODE_1.equals(eval);
    }

    @Override
    public boolean unlock(String lockKey,String lockvalue) {
        String scrip = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object eval = jedis.eval(scrip, Collections.singletonList(lockKey), Collections.singletonList(lockvalue));
        return SUCCESS_CODE_1.equals(eval);
    }
}

通常把lua脚本放到静态文件中,这里就简单硬编码到代码里了。
出了lua脚本,另外redis提供的set key value [EX seconds] [PX milliseconds] [NX|XX]指令也能保证set和expire的原子性。

1.3 分布式锁第三版

public class RedisLock3 implements DistributeLock {
    private Jedis jedis;
    public static final String SUCCESS_CODE_OK = "OK";
    public static final Long SUCCESS_CODE_1 = 1L;

    public RedisLock3() {}

    public RedisLock3(Jedis jedis) {
        this.jedis = jedis;
    }

    @Override
    public boolean lock(String lockKey, String lockvalue, int expire) {
        String result = jedis.set(lockKey, lockvalue, "NX", "EX", 10);
        return SUCCESS_CODE_OK.equals(result);
    }

    @Override
    public boolean unlock(String lockKey,String lockvalue) {
        String scrip = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object eval = jedis.eval(scrip, Collections.singletonList(lockKey), Collections.singletonList(lockvalue));
        return SUCCESS_CODE_1.equals(eval);
    }
}

这种加锁方式就更加方便。
下面再看测试代码。

3. 测试代码

下面开5个线程同时抢锁,设置过期时间10秒

public class TestTask {
    // Runable任务
    class Task implements Runnable{
        // 使用CountDownLatch控制多个线程同时start
        CountDownLatch countDownLatch;
        public Task(CountDownLatch countDownLatch){
            this.countDownLatch = countDownLatch;
        }
        @Override
        public void run() {
            // 每个线程都有各自的redis连接
            Jedis jedis = new Jedis("127.0.0.1", 6379);
            DistributeLock lock = new RedisLock3(jedis);
            // 循环抢锁
            while(true){
                // 用线程名做lockValue,便于清楚看到哪个线程抢到了锁,锁过期时间10秒
                if (lock.lock("order:1:lock",Thread.currentThread().getName(),10)){
                    try {
                        // 等待其他线程准备好
                        countDownLatch.await();
                        Thread.sleep(100);
                        System.out.println("√√√   【"+Thread.currentThread().getName()+"】拿到订单1的");
                        System.out.println("处理业务5秒后释放锁。。。");
                        // 模拟操作共享数据的过程,也就是占用锁时间
                        Thread.sleep(5000);
                        if (lock.unlock("order:1:lock",Thread.currentThread().getName())){
                            System.out.println("×××   【"+Thread.currentThread().getName()+"】释放订单1的");
                            Thread.sleep(2000);
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    private void doTest(){
        final int N = 5; //开5个线程跑任务
        CountDownLatch countDownLatch = new CountDownLatch(N);
        for(int i=0;i<N;i++){
            new Thread(new Task(countDownLatch)).start();
            // 控制多个线程run方法同时执行
            countDownLatch.countDown();
        }
    };

    public static void main(String[] args) {
        TestTask testTask = new TestTask();
        testTask.doTest();
    }
}

测试结果:

√√√   【Thread-1】拿到订单1的
处理业务5秒后释放锁。。。
×××   【Thread-1】释放订单1的
√√√   【Thread-4】拿到订单1的
处理业务5秒后释放锁。。。
×××   【Thread-4】释放订单1的
√√√   【Thread-2】拿到订单1的
处理业务5秒后释放锁。。。
×××   【Thread-2】释放订单1的
√√√   【Thread-1】拿到订单1的
处理业务5秒后释放锁。。。
×××   【Thread-1】释放订单1的
√√√   【Thread-3】拿到订单1的
处理业务5秒后释放锁。。。
×××   【Thread-3】释放订单1的
√√√   【Thread-4】拿到订单1的
处理业务5秒后释放锁。。。

由代码控制拿到锁则可操作共享数据。Redis可以做分布式锁还是基于Redis的零号性能,以及基础指令的天然原子性。以上分布式锁解决方案可以结合AOP用于实际业务中。

你可能感兴趣的:(Redis,redis,分布式锁)