• 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脚本

    作者:以罗伊
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须在文章页面给出原文链接,否则保留追究法律责任的权利。
  • 相关阅读:
    Python自然语言处理学习笔记(6):1.4 回到Python:决策和控制
    Python自然语言处理学习笔记(11):2.3 代码重用
    Python文件夹与文件的操作
    Python自然语言处理学习笔记(10):2.2 条件频率分布
    [导入]SunriseUpload.0.9.1的源码分析(一)
    [导入]Processing A .aspx File From Console Window, Without Using IIS
    [导入]ASP.net(C#)学习要点交流。
    [导入]Access里使用存储过程及用户自己定义的控件里使用参数
    [导入]SunriseUpload.0.9.1的源码分析(五)
    [导入]上传文件时如何保存视图状态?
  • 原文地址:https://www.cnblogs.com/my-ordinary/p/12658347.html
Copyright © 2020-2023  润新知