User-Profile-Image
hankin
  • 5
  • Java
  • Kotlin
  • Spring
  • Web
  • SQL
  • MegaData
  • More
  • Experience
  • Enamiĝu al vi
  • 分类
    • Zuul
    • Zookeeper
    • XML
    • WebSocket
    • Web Notes
    • Web
    • Vue
    • Thymeleaf
    • SQL Server
    • SQL Notes
    • SQL
    • SpringSecurity
    • SpringMVC
    • SpringJPA
    • SpringCloud
    • SpringBoot
    • Spring Notes
    • Spring
    • Servlet
    • Ribbon
    • Redis
    • RabbitMQ
    • Python
    • PostgreSQL
    • OAuth2
    • NOSQL
    • Netty
    • MySQL
    • MyBatis
    • More
    • MinIO
    • MegaData
    • Maven
    • LoadBalancer
    • Kotlin Notes
    • Kotlin
    • Kafka
    • jQuery
    • JavaScript
    • Java Notes
    • Java
    • Hystrix
    • Git
    • Gateway
    • Freemarker
    • Feign
    • Eureka
    • ElasticSearch
    • Docker
    • Consul
    • Ajax
    • ActiveMQ
  • 页面
    • 归档
    • 摘要
    • 杂图
    • 问题随笔
  • 友链
    • Spring Cloud Alibaba
    • Spring Cloud Alibaba - 指南
    • Spring Cloud
    • Nacos
    • Docker
    • ElasticSearch
    • Kotlin中文版
    • Kotlin易百
    • KotlinWeb3
    • KotlinNhooo
    • 前端开源搜索
    • Ktorm ORM
    • Ktorm-KSP
    • Ebean ORM
    • Maven
    • 江南一点雨
    • 江南国际站
    • 设计模式
    • 熊猫大佬
    • java学习
    • kotlin函数查询
    • Istio 服务网格
    • istio
    • Ktor 异步 Web 框架
    • PostGis
    • kuangstudy
    • 源码地图
    • it教程吧
    • Arthas-JVM调优
    • Electron
    • bugstack虫洞栈
    • github大佬宝典
    • Sa-Token
    • 前端技术胖
    • bennyhuo-Kt大佬
    • Rickiyang博客
    • 李大辉大佬博客
    • KOIN
    • SQLDelight
    • Exposed-Kt-ORM
    • Javalin—Web 框架
    • http4k—HTTP包
    • 爱威尔大佬
    • 小土豆
    • 小胖哥安全框架
    • 负雪明烛刷题
    • Kotlin-FP-Arrow
    • Lua参考手册
    • 美团文章
    • Java 全栈知识体系
    • 尼恩架构师学习
    • 现代 JavaScript 教程
    • GO相关文档
    • Go学习导航
    • GoCN社区
    • GO极客兔兔-案例
    • 讯飞星火GPT
    • Hollis博客
    • PostgreSQL德哥
    • 优质博客推荐
    • 半兽人大佬
    • 系列教程
    • PostgreSQL文章
    • 云原生资料库
    • 并发博客大佬
Help?

Please contact us on our email for need any support

Support
    首页   ›   SQL   ›   NOSQL   ›   Redis   ›   正文
Redis

Redis笔记—分布式锁

2020-04-04 01:50:57
828  0 1
参考目录 隐藏
1) 解决超时问题
2) Lua 脚本的优势:
3) Redis redis = new Redis() 连接的参考:
4) 补充:
5) redis内置lua执行命令
6) EVAL命令语法
7) EVALSHA 命令语法
8) Redis中管理Lua脚本
9) lua函数

阅读完需:约 10 分钟

分布式锁实现的思路很简单,就是进来一个线程先占位,当别的线程进来操作时,发现已经有人占位 了,就会放弃或者稍后再试。

在 Redis 中,占位一般使用 setnx 指令,先进来的线城先占位,线城的操作执行完成后,再调用 del 指 令释放位子。

根据上面的思路,我们写出的代码如下:

public interface CallWithJedis {    
    void call(Jedis jedis);
}
public class Redis {    
    private JedisPool pool;
    public Redis() {        
        GenericObjectPoolConfig config = new GenericObjectPoolConfig(); 
        //连接池最大空闲数        
        config.setMaxIdle(300);        
        //最大连接数        
        config.setMaxTotal(1000);        
        //连接最大等待时间,如果是 -1 表示没有限制        
        config.setMaxWaitMillis(30000);        
        // 在空闲时检查有效性        
        config.setTestOnBorrow(true);        
        /** * 1. Redis 地址  * 2. Redis 端口   * 3. 连接超时时间  * 4. 密码   */ 
        pool = new JedisPool(config, "192.168.91.128", 6379, 30000, "javaboy");  
 }
    public void execute(CallWithJedis callWithJedis) {        
        try (Jedis jedis = pool.getResource()) {            
            callWithJedis.call(jedis);      
        }  
    }
}
public class LockTest {
    public static void main(String[] args) {
        Redis redis = new Redis();
        redis.execute(jedis->{
            Long setnx = jedis.setnx("k1", "v1");
            if (setnx == 1) {
                //没人占位
                jedis.set("name", "javaboy");
                String name = jedis.get("name");
                System.out.println(name);
                jedis.del("k1");//释放资源
            }else{
                //有人占位,停止/暂缓 操作
            }
        });
    }
 }

上面的代码存在一个小小问题:如果代码业务执行的过程中抛异常或者挂了,这样会导致 del 指令没有 被调用,这样,k1 无法释放,后面来的请求全部堵塞在这里,锁也永远得不到释放。
要解决这个问题,我们可以给锁添加一个过期时间,确保锁在一定的时间之后,能够得到释放。改进后 的代码如下:

public class LockTest {
    public static void main(String[] args) {
        Redis redis = new Redis();
        redis.execute(jedis->{
            Long setnx = jedis.setnx("k1", "v1");
            if (setnx == 1) {
                //给锁添加一个过期时间,防止应用在运行过程中抛出异常导致锁无法及时得到释放
                jedis.expire("k1", 5);
                //没人占位
                jedis.set("name", "javaboy");
                String name = jedis.get("name");
                System.out.println(name);
                jedis.del("k1");//释放资源
            }else{
                //有人占位,停止/暂缓 操作
            }
        });
    }
 }

这样改造之后,还有一个问题,就是在获取锁和设置过期时间之间如果如果服务器突然挂掉了,这个时 候锁被占用,无法及时得到释放,也会造成死锁,因为获取锁和设置过期时间是两个操作,不具备原子 性。
为了解决这个问题,从 Redis2.8 开始,setnx 和 expire 可以通过一个命令一起来执行了,我们对上述 代码再做改进:

public class LockTest {
    public static void main(String[] args) {
        Redis redis = new Redis();
        redis.execute(jedis->{
            String set = jedis.set("k1", "v1", new SetParams().nx().ex(5));
            if (set !=null && "OK".equals(set)) {
                //给锁添加一个过期时间,防止应用在运行过程中抛出异常导致锁无法及时得到释放
                jedis.expire("k1", 5);
                //没人占位
                jedis.set("name", "javaboy");
                String name = jedis.get("name");
                System.out.println(name);
                jedis.del("k1");//释放资源
            }else{
                //有人占位,停止/暂缓 操作
            }
        });
    }
 } 

解决超时问题

为了防止业务代码在执行的时候抛出异常,我们给每一个锁添加了一个超时时间,超时之后,锁会被自 动释放,但是这也带来了一个新的问题:如果要执行的业务非常耗时,可能会出现紊乱。举个例子:第 一个线程首先获取到锁,然后开始执行业务代码,但是业务代码比较耗时,执行了 8 秒,这样,会在第 一个线程的任务还未执行成功锁就会被释放了,此时第二个线程会获取到锁开始执行,在第二个线程刚 执行了 3 秒,第一个线程也执行完了,此时第一个线程会释放锁,但是注意,它释放的第二个线程的 锁,释放之后,第三个线程进来。

对于这个问题,我们可以从两个角度入手:

  • 尽量避免在获取锁之后,执行耗时操作。
  • 可以在锁上面做文章,将锁的 value 设置为一个随机字符串,每次释放锁的时候,都去比较随机 字符串是否一致,如果一致,再去释放,否则,不释放。

对于第二种方案,由于释放锁的时候,要去查看锁的 value,第二个比较 value 的值是否正确,第三步 释放锁,有三个步骤,很明显三个步骤不具备原子性,为了解决这个问题,我们得引入 Lua 脚本。

Lua 脚本的优势:

  • 使用方便,Redis 中内置了对 Lua 脚本的支持。
  • Lua 脚本可以在 Redis 服务端原子的执行多个 Redis 命令。
  • 由于网络在很大程度上会影响到 Redis 性能,而使用 Lua 脚本可以让多个命令一次执行,可以有 效解决网络给 Redis 带来的性能问题。

在 Redis 中,使用 Lua 脚本,大致上两种思路:

  1. 提前在 Redis 服务端写好 Lua 脚本,然后在 Java 客户端去调用脚本(推荐)。
  2. 可以直接在 Java 端去写 Lua 脚本,写好之后,需要执行时,每次将脚本发送到 Redis 上去执行。

首先在 Redis 服务端创建 Lua 脚本,内容如下:

if redis.call("get",KEYS[1])==ARGV[1] then
   return redis.call("del",KEYS[1])
else
   return 0 
end

接下来,可以给 Lua 脚本求一个 SHA1 和,命令如下:

cat lua/releasewherevalueequal.lua | redis-cli -a javaboy script load --pipe

script load 这个命令会在 Redis 服务器中缓存 Lua 脚本,并返回脚本内容的 SHA1 校验和,然后在 Java 端调用时,传入 SHA1 校验和作为参数,这样 Redis 服务端就知道执行哪个脚本了。

如果是windows安装了Redis可是使用以下命令:

script load 'if redis.call("get",KEYS[1])==ARGV[1] then   return redis.call("del",KEYS[1]) else   return 0 end'

接下来,在 Java 端调用这个脚本。

public class LuaTest {
    public static void main(String[] args) {
        Redis redis = new Redis();
        for (int i = 0; i < 2; i++) {
            redis.execute(jedis -> {
                //1.先获取一个随机字符串
                //UUID:在一台机器上生成的数字,它保证对在同一时空中的所有机器都是唯一的
                String value = UUID.randomUUID().toString();
                //2.获取锁
                String k1 = jedis.set("k1", value, new SetParams().nx().ex(5));
                //3.判断是否成功拿到锁
                if (k1 != null && "OK".equals(k1)) {
                    //4. 具体的业务操作
                    jedis.set("site", "www.javaboy.org");
                    String site = jedis.get("site");
                    System.out.println(site);
                    //5.释放锁
                    //Arrays.asList:Java数组转换为List
                   jedis.evalsha("b8059ba43af6ffe8bed3db65bac35d452f8115d8", Arrays.asList("k1"), Arrays.asList(value));
                } else {
                    System.out.println("没拿到锁");
                }
            });
        }
    }
 }  

Redis redis = new Redis() 连接的参考:

Redis笔记—连接 Java 客户端(Jedis)

补充:

redis内置lua执行命令

EVAL命令语法

EVAL script numkeys key [key …] arg [arg …]
EVAL —lua程序的运行环境上下文
script —lua脚本
numkeys —参数的个数(key的个数)
key —redis键 访问下标从1开始,例如:KEYS[1]
arg —redis键的附加参数

EVALSHA 命令语法

EVALSHA SHA1 numkeys key [key …] arg [arg …]
EVALSHA命令允许通过脚本的SHA1来执行(节省带宽)

Redis在执行EVAL/SCRIPT LOAD后会计算脚本SHA1缓存, EVALSHA根据SHA1取出缓存脚本执行.

Redis中管理Lua脚本

script load script 将Lua脚本加载到Redis内存中(如果redis重启则会丢失)
script exists sh1 [sha1 …] 判断sha1脚本是否在内存中
script flush 清空Redis内存中所有的Lua脚本
script kill 杀死正在执行的Lua脚本。(如果此时Lua脚本正在执行写操作,那么script kill将不会生效)
Redis提供了一个lua-time-limit参数,默认5秒,它是Lua脚本的超时时间,如果Lua脚本超时,其他执行正常命令的客户端会收到“Busy Redis is busy running a script”错误,但是不会停止脚本运行,此时可以使用script kill 杀死正在执行的Lua脚本。

lua函数

主要有两个函数来执行redis命令
redis.call() – 出错时返回具体错误信息,并且终止脚本执行
redis.pcall() –出错时返回lua table的包装错误,但不引发错误

如本文“对您有用”,欢迎随意打赏作者,让我们坚持创作!

1 打赏
Enamiĝu al vi
不要为明天忧虑.因为明天自有明天的忧虑.一天的难处一天当就够了。
543文章 68评论 292点赞 582259浏览

随机文章
Spring笔记
5年前
Java—并发编程(六)JUC锁 – (12) Phaser
3年前
FastDFS—— 构建分布式文件管理系统
5年前
WebSocket(四)—五子棋
5年前
Spring—AOP切面(构建)
3年前
博客统计
  • 日志总数:543 篇
  • 评论数目:68 条
  • 建站日期:2020-03-06
  • 运行天数:1904 天
  • 标签总数:23 个
  • 最后更新:2024-12-20
Copyright © 2025 网站备案号: 浙ICP备20017730号 身体没有灵魂是死的,信心没有行为也是死的。
主页
页面
  • 归档
  • 摘要
  • 杂图
  • 问题随笔
博主
Enamiĝu al vi
Enamiĝu al vi 管理员
To be, or not to be
543 文章 68 评论 582259 浏览
测试
测试
看板娘
赞赏作者

请通过微信、支付宝 APP 扫一扫

感谢您对作者的支持!

 支付宝 微信支付