08-Redis 应用-分布式锁

文章目录

  • Redis实现分布式锁
    • 一、分布式锁实现对比
    • 二、基于Redis实现
      • 2.1 原理
      • 2.2 细节
        • 2.2.1 加锁
        • 2.2.2 解锁
      • 2.3 实现代码
      • 2.4 测试
    • 三、小结

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实现分布式锁,注意事项如下
操作 注意事项
加锁 必须设置失效时间避免死锁。
加锁 写入随机值,避免锁误删。
加锁 写入值和设置失效时间必须是一个原子操作,保证加锁是原子的
解锁 保证获取数据,判断,删除节点这三个动作是原子的

你可能感兴趣的:(Redis和缓存)