08-Redis 应用-分布式锁

Redis实现分布式锁

  • 分布式锁应用场景
1.多任务环境
2.多任务对共享资源访问
3.共享资源访问是互斥的(比如读就不是互斥的,写是互斥的)

一、分布式锁实现对比

  • 分布式锁常用的方案如下:
方案实现思路优点缺点
基于mysql利用数据库的行锁机制简单性能差,容易死锁
基于redis基于redis的setnx命令,lua保证原子性性能好实现相对复杂
基于zk基于zk节点的原子特性和watch机制性能好,稳定可靠,可较好的实现阻塞锁实现较复杂

二、基于Redis实现

2.1 原理

  • 使用setnx这个命令(不存在才设置),命令是原子的。设置值成功代表加锁成功,后面的线程设置就会失败。
  • 使用lua脚本,脚本内的执行逻辑是原子性的,和一条命令一样,因此可以将多个命令组合成一个原子命令
  • 另外用到了key的自动过期

2.2 细节

2.2.1 加锁
  • 使用setnx向特定的key写入一个随机值并设置失效时间,成功代表加锁成功
必须设置失效时间,避免死锁。比如服务突然宕机,没有失效时间的话,节点永远不会被删除,那么其他线程都获取不到锁,死锁。
写入随机值,避免锁误删。比如失效时间是3S,业务一般处理只需要几十毫秒,某一次业务发生异常耗费了5S,再去解锁的时候,实际上此时自己设置的值已经过期删除了,此时的key是另一个线程加锁设置的,那么自己肯定不能把另一个线程加锁的值给删除,通过随机值value的匹配来避免。
写入值和设置失效时间必须是一个原子操作,保证加锁是原子的

set key value nx px 10000 (不能分几次命令操作,那样不是原子性的)
2.2.2 解锁
  • 获取指定key数据,判断和自己加锁时的随机值是否一致,匹配一致就删除节点。保证获取数据,判断,删除这三个动作是原子的
//因为key是不变的,每个线程加锁的时候,设置的值不一样,因此值在不断变化,如果步骤1和2不是原子的,可能获取的时候是一致
//的,判断的时候已经更改了。使用lua脚本实现:
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1]);
else
    return 0;
end

2.3 实现代码

public class MyRedisLock {

    //使用threadLocal来保存每个线程加锁的时候生成的随机数
    private static ThreadLocal<String> local = new ThreadLocal<>();
    private static final String KEY = "KEY";

    //简单实现了阻塞锁,自旋直到获取锁成功
    public static boolean lock() {
        for (; ; ) {
            if (tryLock()) {
                return true;
            }
        }
    }

    public static boolean tryLock() {
        String uuid = UUID.randomUUID().toString();
        String ret = JedisFactory.getJedis().set(KEY, uuid, "NX", "PX", 3000);
        if ("OK".equals(ret)) {
            local.set(uuid);
            return true;
        }
        return false;
    }

    public static void unLock() {
        String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" +
                "    return redis.call(\"del\",KEYS[1]);\n" +
                "else\n" +
                "    return 0;\n" +
                "end";
        //从threadLocal中获取本线程加锁的时候设置的随机数
        String value = local.get();
        JedisFactory.getJedis().eval(script, Arrays.asList(KEY), Arrays.asList(value));
    }
}

2.4 测试

  • 测试类:
  • 实际上应该使用5个进程来测试,因为分布式锁实际上是对于跨进程而言的,如果仅仅只有线程之间可以使用JDK中的锁,但是跨线程也可以理解为跨进程的
    一种特殊情况,如果分布式锁生效,那么跨进程可以生效,那么夸线程也是肯定可以生效的,这个锁实际上是在远程"锁"住的
public class MyRedisLockTest {

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            //创建5个执行线程
            new MyThread("Thread-" + i).start();
        }
    }

    static class MyThread extends Thread {

        public MyThread(String name) {
            super(name);
        }

        @Override
        public void run() {
            MyRedisLock.lock();
            try {
                System.out.println("Thread " + Thread.currentThread().getName() +
                        " do something thing..." + new Date());
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                MyRedisLock.unLock();
            }
        }
    }
}
  • 输出(5个线程排队执行,锁生效了):
Thread Thread-0 do something thing...Sat Jun 15 23:14:52 CST 2019
Thread Thread-2 do something thing...Sat Jun 15 23:14:54 CST 2019
Thread Thread-1 do something thing...Sat Jun 15 23:14:56 CST 2019
Thread Thread-4 do something thing...Sat Jun 15 23:14:58 CST 2019
Thread Thread-3 do something thing...Sat Jun 15 23:15:00 CST 2019

三、小结

  • 本文主要梳理了基于redis实现分布式锁,从原理到细节,到代码,测试。
  • redis实现分布式锁,注意事项如下
操作注意事项
加锁必须设置失效时间避免死锁。
加锁写入随机值,避免锁误删。
加锁写入值和设置失效时间必须是一个原子操作,保证加锁是原子的
解锁保证获取数据,判断,删除节点这三个动作是原子的
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值