redis+lua脚本 分布式锁初步学习

0 环境

  • 系统环境: centos7
  • 编辑器: xshell和IDEA

1 前言

常见场景:

在单线程中 用户操作 一个线程修改用户状态 1 从数据库中读取用户状态 2 在内存中进行修改 3 修改好后 在重新写入 但在多线程中 读 改 写是三个操作 非原子操作 会出现问题

2 准备

目录结构
代码结构 参考3-3节对连接池强约束

3 基本使用

  • 一路畅通

先进来一个线程抢占位置 当其他线程进来操作时 发现已经有人占位了 放弃/等待(古语说的好 占坑那啥的 很贴切 setnx结束了 用del释放)

    /** 
    * @Description: v1版本 --> 分布式锁 先来一个人占位 其他人 等着吧 直到这个人走了
    * @Param:  
    * @return:  
    * @Author: 水面行走
    * @Date: 2020/4/6
    */
    private static void test() {
        CallRedisDemo redisDemo = new CallRedisDemo();
        redisDemo.execute(jedis -> {
            // redis中setnx方法 当key为空时 创建 1
            Long k = jedis.setnx("k", "123");

            if (1 == k) {
                System.out.println("新建key:" + jedis.exists("k"));

                // 赋值
                jedis.set("age", "15");
                System.out.println("获取age:" + jedis.get("age"));

                // 使命完成 销毁
                jedis.del("k");

                System.out.println("删除key:" + jedis.exists("k"));
            }else {
                // 有人占位 等待
            }


        });
    }

注销jedis.del("k");或是添加休眠时间 就可以看到k是存到redis中的

  • 添加过期时间

若是在执行时 阻塞了 添加一个过期时间 哪怕阻塞 过期释放

/**
     * @Description: v2版本 --> v1版本(中间无问题 一路畅通) 但总会有点跌跌撞撞的
     *                 假若出现异常 到不了删除key/执行错误等 key无法释放 请求阻塞
     *                 需要一个过期命令 例如食物有过期时间一样
     * @Param:
     * @return:
     * @Author: 水面行走
     * @Date: 2020/4/6
     */
    private static void test1() {
        CallRedisDemo redisDemo = new CallRedisDemo();
        redisDemo.execute(jedis -> {
            // redis中setnx方法 当key为空时 创建 1
            Long k = jedis.setnx("k", "123");

            if (1 == k) {
                // 设置过期时间
                jedis.expire("k", 6);

                System.out.println("新建key:" + jedis.exists("k"));

                // 赋值
                jedis.set("age", "15");
                System.out.println("获取age:" + jedis.get("age"));

                // 使命完成 销毁
//                jedis.del("k");

                System.out.println("删除key:" + jedis.exists("k"));
            }else {
                // 有人占位 等待
            }


        });
    }


    /**
     * @Description: v2.1版本 --> v2版本 还是会有问题 若是在shenx和设置过期时间(两个操作)之间出现异常
     *                  假如服务器挂了等 锁还在 未释放资源 解决问题(将2个操作合为1个操作 从redis2.8开始)
     *
     * @Param:
     * @return:
     * @Author: 水面行走
     * @Date: 2020/4/6
     */
    private static void test2() {
        CallRedisDemo redisDemo = new CallRedisDemo();
        redisDemo.execute(jedis -> {
            // redis中setnx方法 当key为空时 创建 1 设置过期时间
            String k = jedis.set("k", "123", new SetParams().nx().ex(9));

            if (null != k && k.equals("OK")) {

                System.out.println("新建key:" + jedis.exists("k"));

                // 赋值
                jedis.set("age", "15");
                System.out.println("获取age:" + jedis.get("age"));

                // 使命完成 销毁
                jedis.del("k");

                System.out.println("删除key:" + jedis.exists("k"));
            }else {
                // 有人占位 等待
            }


        });
    }

4 超时时间解决

  • 在centos7中 新建个目录 创建xxx.lua脚本 例如
mkdir redis-lua
vim test.lua
  • 编写调用方式(test.lua)好后 wq保存
if redis.call("get",KEYS[1]) == ARGV[1] then
   return redis.call("del", KEYS[1])
else
   return 0
end
  • 将lua脚本加载到redis中
    cat springcloud/lua/redis-lua/test.lua | redis-cli -a 123456 script load --pipe

script load缓存lua脚本 并将返回SHA1校验和 复制到java中evalsha参数里

 /**
     * @Description: v2.2版本 --> 超时问题
     *                假如A获得锁 --> 由于处理时间长 需要13s 导致在未完成的情况下 释放了A锁
     *                这时B拿到了锁 刚执行5s A完成操作 释放B锁
     *                C拿到锁 运行 B完成操作 释放C锁
     *
     *                思路:
     *                  1 对于耗时业务 避免使用锁
     *                  2 在锁上下功夫了 给锁加一个保安(value设置随机字符串) 你要出去是吧
     *                    说一下你的 对比一下(随机字符串与redis中的对比) 一致你出去吧
     *                    否则 继续在家呆着 瞎跑啥
     *
     *                方案:选择redis中的lua脚本 其原子性 缓存特性 操作性 使用方便 内置包不错
     *
     *                lua优点:
     *                  1 使用方便
     *                  2 其原子性可执行多个redis命令
     *                  3 对于网络性能 一次多个命令一起执行 更好 类似cloud中的 请求合并
     *
     *                使用lua脚本:
     *                  1 在服务端写好lua脚本 客户端调用
     *                  2 在客户端编写lua脚本 执行时发送给服务器端的redis上执行
     *
     * @Param:
     * @return:
     * @Author: 水面行走
     * @Date: 2020/4/6
     */
    private static void test3() {
        CallRedisDemo redisDemo = new CallRedisDemo();
        redisDemo.execute(jedis -> {
            
                // 设置随机数
                String randVal = UUID.randomUUID().toString();

                // redis中setnx方法 当key为空时 创建 1 设置过期时间
                String k = jedis.set("k", randVal, new SetParams().nx().ex(9));

                if (null != k && k.equals("OK")) {

                    System.out.println("新建key:" + jedis.exists("k"));

                    // 赋值
                    jedis.set("age", "15");
                    System.out.println("获取age:" + jedis.get("age"));

                    // 使命完成 销毁
//                jedis.del("k");

//                    System.out.println("删除key:" + jedis.exists("k"));

                    // 释放锁 随机数与redis中的比对
                    jedis.evalsha("b8059ba43af6ffe8bed3db65bac35d452f8115d8", Arrays.asList("k"), Arrays.asList(randVal));

                    System.out.println("通过");

                }else {
                    System.out.println("没有拿到锁");
                }
            
        });
    }

  • 加个for循环呢
/**
     * @Description: v2.2版本 --> 超时问题
     *                假如A获得锁 --> 由于处理时间长 需要13s 导致在未完成的情况下 释放了A锁
     *                这时B拿到了锁 刚执行5s A完成操作 释放B锁
     *                C拿到锁 运行 B完成操作 释放C锁
     *
     *                思路:
     *                  1 对于耗时业务 避免使用锁
     *                  2 在锁上下功夫了 给锁加一个保安(value设置随机字符串) 你要出去是吧
     *                    说一下你的 对比一下(随机字符串与redis中的对比) 一致你出去吧
     *                    否则 继续在家呆着 瞎跑啥
     *
     *                方案:选择redis中的lua脚本 其原子性 缓存特性 操作性 使用方便 内置包不错
     *
     *                lua优点:
     *                  1 使用方便
     *                  2 其原子性可执行多个redis命令
     *                  3 对于网络性能 一次多个命令一起执行 更好 类似cloud中的 请求合并
     *
     *                使用lua脚本:
     *                  1 在服务端写好lua脚本 客户端调用
     *                  2 在客户端编写lua脚本 执行时发送给服务器端的redis上执行
     *
     * @Param:
     * @return:
     * @Author: 水面行走
     * @Date: 2020/4/6
     */
    private static void test3() {
        CallRedisDemo redisDemo = new CallRedisDemo();
        int count = 1;
        for (int i = 0; i < 2; i++) {
            System.out.println(count++);
            redisDemo.execute(jedis -> {
    
                // 设置随机数
                String randVal = UUID.randomUUID().toString();

                // redis中setnx方法 当key为空时 创建 1 设置过期时间
                String k = jedis.set("k", randVal, new SetParams().nx().ex(9));

                if (null != k && k.equals("OK")) {

                    System.out.println("新建key:" + jedis.exists("k"));

                    // 赋值
                    jedis.set("age", "15");
                    System.out.println("获取age:" + jedis.get("age"));

                    // 使命完成 销毁
//                jedis.del("k");

//                    System.out.println("删除key:" + jedis.exists("k"));

                    // 释放锁 随机数与redis中的比对
                    jedis.evalsha("b8059ba43af6ffe8bed3db65bac35d452f8115d8", Arrays.asList("k"), Arrays.asList(randVal));

                    System.out.println("通过");

                } else {
                    System.out.println("没有拿到锁");
                }
            });
        }

    }

  • 验证释放
/**
     * @Description: v2.2版本 --> 超时问题
     *                假如A获得锁 --> 由于处理时间长 需要13s 导致在未完成的情况下 释放了A锁
     *                这时B拿到了锁 刚执行5s A完成操作 释放B锁
     *                C拿到锁 运行 B完成操作 释放C锁
     *
     *                思路:
     *                  1 对于耗时业务 避免使用锁
     *                  2 在锁上下功夫了 给锁加一个保安(value设置随机字符串) 你要出去是吧
     *                    说一下你的 对比一下(随机字符串与redis中的对比) 一致你出去吧
     *                    否则 继续在家呆着 瞎跑啥
     *
     *                方案:选择redis中的lua脚本 其原子性 缓存特性 操作性 使用方便 内置包不错
     *
     *                lua优点:
     *                  1 使用方便
     *                  2 其原子性可执行多个redis命令
     *                  3 对于网络性能 一次多个命令一起执行 更好 类似cloud中的 请求合并
     *
     *                使用lua脚本:
     *                  1 在服务端写好lua脚本 客户端调用
     *                  2 在客户端编写lua脚本 执行时发送给服务器端的redis上执行
     *
     * @Param:
     * @return:
     * @Author: 水面行走
     * @Date: 2020/4/6
     */
    private static void test3() throws InterruptedException {
        CallRedisDemo redisDemo = new CallRedisDemo();
        int count = 1;
        for (int i = 0; i < 2; i++) {
            System.out.println(count++);
            redisDemo.execute(jedis -> {

                // 设置随机数
                String randVal = UUID.randomUUID().toString();

                // redis中setnx方法 当key为空时 创建 1 设置过期时间
                String k = jedis.set("k", randVal, new SetParams().nx().ex(9));

                if (null != k && k.equals("OK")) {

                    System.out.println("新建key:" + jedis.exists("k"));

                    // 赋值
                    jedis.set("age", "15");
                    System.out.println("获取age:" + jedis.get("age"));

                    // 使命完成 销毁
//                jedis.del("k");

//                    System.out.println("删除key:" + jedis.exists("k"));

                    // 释放锁 随机数与redis中的比对
                    jedis.evalsha("b8059ba43af6ffe8bed3db65bac35d452f8115d8", Arrays.asList("k"), Arrays.asList(randVal));

                    System.out.println("通过");

                } else {
                    System.out.println("没有拿到锁");
                }
            });

            // 添加休眠时间 让其真正释放
            Thread.sleep(9000);
        }

    }

5 小结

分布式锁代码下载

从开始的setnx 到过期时间的配置 更换为具体原子性的lua脚本

posted @ 2020-04-08 15:59  焜掱玚  阅读(351)  评论(0编辑  收藏  举报
levels of contents