Redis高并发分布式锁实战

1.分布式场景下的synchronized失效的问题–用redis实现分布式锁

synchronized是通过monitor实现的jvm级别的锁,如果是分布式系统,跑在不同的虚拟机上的tomcat上,会导致synchronized无法锁住对象 ----------- 需要分布式锁 redis

SET、SETEX、SETNX

SET key value
含义:

     SET KEY value  V-K相同的K 后写的覆盖先写的

SETEX key seconds value
该命令相当于将下面两行操作合并为一个原子操作

SET key value
EXPIRE key seconds # 设置生存时间
含义(setex = set expire):

          SET KEY value  V-K 设置生命周期 相同的K 后写的覆盖先写的

SETNX key value
含义(setnx = SET if Not eXists):

       SET KEY value  V-K  ,key 不存在返回1 表示成功,key存在返回0SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。

返回值:

       设置成功,返回 1 。设置失败,返回 0 。

2.redis实现分布式锁

@RequestMapping("/redis-001")
public String redis001() {String key = "redis-001";Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(key, "redis-001");if (!result) {return "error_code";}int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));if (stock > 0) {int realStock = stock - 1;stringRedisTemplate.opsForValue().set("stock", realStock + "");System.out.println("扣减成功,剩余库存:" + realStock);} else {System.out.println("扣减失败,库存不足");}stringRedisTemplate.delete(key);return "end";
}

在 stringRedisTemplate.delete(key) 释放锁之前会有业务代码块,若出现异常抛出,则不能执行关锁的代码块

@RequestMapping("/redis-001")
public String redis001() {String key = "redis-001";Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(key, "redis-001");if (!result) {return "error_code";}try {int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));if (stock > 0) {int realStock = stock - 1;stringRedisTemplate.opsForValue().set("stock", realStock + "");System.out.println("扣减成功,剩余库存:" + realStock);} else {System.out.println("扣减失败,库存不足");}} finally {stringRedisTemplate.delete(key);}return "end";
}

在程序执行的任意时刻都有可能应为不可抗力因素突然终止,重启、宕机导致不能执行到finally代码块,所以必须要设置超时时间

    @RequestMapping("/redis-001")public String redis001() {String key = "redis-001";Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(key, "redis-001");stringRedisTemplate.expire(key,2000 ,TimeUnit.MILLISECONDS);if (!result) {return "error_code";}try {int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));if (stock > 0) {int realStock = stock - 1;stringRedisTemplate.opsForValue().set("stock", realStock + "");System.out.println("扣减成功,剩余库存:" + realStock);} else {System.out.println("扣减失败,库存不足");}} finally {stringRedisTemplate.delete(key);}return "end";}

redis设置的时候需要保证原子性

Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(key, "redis-001");
stringRedisTemplate.expire(key,2000 ,TimeUnit.MILLISECONDS);

解决方案

Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(key, clintId,2000 ,TimeUnit.MILLISECONDS);

若T1的锁设置失效时间20s,但是T1执行20s没有完成,此时T2可以获得锁,T1执行会在finally代码块中释放T2加的锁

    @RequestMapping("/redis-001")public String redis001() {String key = "redis-001";//不满足原子性
//        Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(key, "redis-001");
//        stringRedisTemplate.expire(key,2000 ,TimeUnit.MILLISECONDS);String clintId = UUID.randomUUID().toString();//Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(key, "redis-001",2000 ,TimeUnit.MILLISECONDS);Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(key, clintId,2000 ,TimeUnit.MILLISECONDS);if (!result) {return "error_code";}try {int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));if (stock > 0) {int realStock = stock - 1;stringRedisTemplate.opsForValue().set("stock", realStock + "");System.out.println("扣减成功,剩余库存:" + realStock);} else {System.out.println("扣减失败,库存不足");}} finally {if (clintId.equals(stringRedisTemplate.opsForValue().get(key))){stringRedisTemplate.delete(key);}}return "end";}
finally {if (clintId.equals(stringRedisTemplate.opsForValue().get(key))){stringRedisTemplate.delete(key);}}

上述代码来确定是否是自己的锁,是没有原子性的,使用redisson(lua)来解决这个问题

Redisson

Redis 自 2.6 版本开始支持 Lua 脚本,是将所有操作打包成原子操作的一种机制。使用 Lua 脚本可以对 Redis 数据库进行复杂的操作,比如多个命令组合执行、避免分布式事务中的竞态条件等。

Lua 脚本在 Redis 中的原理是:将脚本发送到 Redis 服务器时,Redis 会先对脚本进行语法检查和编译,然后将编译后的字节码缓存起来并返回一个 SHA1 校验和。之后客户端每次需要执行这个脚本时,只需要将 SHA1 校验和发送给 Redis 服务器,Redis 通过校验和即可直接获取缓存中的字节码,避免了每次解析和编译 Lua 脚本的开销。

Lua 脚本的好处有以下几点:

  1. 原子性:Lua 脚本是 Redis 支持的最完整的事务形式,因为它们在 Redis 服务器上作为一个单独的脚本条目执行,因此能够保证所有操作的原子性。

  2. 灵活性:Lua 脚本方便对于 Redis 数据库进行复杂的操作,比如批量操作等。

  3. 性能:由于 Redis 会对 Lua 脚本进行预编译并缓存字节码,因此当相同的脚本被多次执行时,可以避免每次解析和编译脚本带来的开销。而且,Lua 脚本在 Redis 服务器中以单线程运行,相比于多线程,这样可以减少线程切换、锁等开销。

总之,Redis Lua 脚本具有良好的性能、灵活性和原子性,使得 Redis 支持更复杂、更安全地操作数据,提高了 Redis 在实际应用中的可靠性和稳定性。

Lua脚本语法

Redisson.lock()

 <T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {internalLockLeaseTime = unit.toMillis(leaseTime);return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,"if (redis.call('exists', KEYS[1]) == 0) then " +"redis.call('hset', KEYS[1], ARGV[2], 1); " +"redis.call('pexpire', KEYS[1], ARGV[1]); " +"return nil; " +"end; " +"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +"redis.call('pexpire', KEYS[1], ARGV[1]); " +"return nil; " +"end; " +"return redis.call('pttl', KEYS[1]);",Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));}
getName() //RLock redissonLock = redisson.getLock(lockKey);
internalLockLeaseTime //internalLockLeaseTime = unit.toMillis(leaseTime);->RFuture<Boolean> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN); //getLockWatchdogTimeout() 默认30s getLockName(threadId)  //final UUID id + threadId

设置锁成功如何执行看门狗机制实现续命

 RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);ttlRemainingFuture.addListener(new FutureListener<Long>() {@Overridepublic void operationComplete(Future<Long> future) throws Exception {if (!future.isSuccess()) {return;}Long ttlRemaining = future.getNow(); //成功是nil 即null// lock acquiredif (ttlRemaining == null) {//成功一定进入的代码块scheduleExpirationRenewal(threadId);}}});

没有获得锁的线程自旋等待,间歇性尝试加锁

getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS); //是阻塞等待不会占用资源

ttl时间内如果当前获得锁的线程执行完成了怎么办,订阅模式

订阅的内容将在删除的时候更新

unlock使用了lua代码保证了原子操作

1.集群架构下锁失效

产生的原因是master节点加锁成功,异步同步给从节点之前主节点挂了,从节点被重新选举成为新的主节点,但是新的线程加锁的时候,可以获得锁。

zookeeper的解决思路:

zookeeper(cp)确认机制,master设置成功不会马上返回,需要和从节点发起同步,从节点半数以上的同步完成后才会返回给master

redis的解决思路:

redis 可以使用redLock redisSon中就有实现,采用的和zookeeper使用的机制是类似的也是需要收到半数以上的从节点的确认消息才会返回给主节点,才会返回成功,但是这牺牲了可用性的同时也不会(没有zk严谨)

redLock的问题

1.三个redis组成集群架构为了保证高可用给每个节点挂至少一个从节点,客户端加锁 A 确认成功、同步成功,B确认成功同步不成功(B挂了)的情况下,客户端认为加锁成功,此时客户端再次加锁,新的B、C可以加锁,出现了问题

(补充图片)

2.同步从节点会出现问题,呢么我不要从节点了,如3节点的情况,挂了两个节点的时候就失去了对外提供服务的能力,redis不可用

3.3个节点挂两个就不能对外提供服务呢我使用多个节点呢?这样可以一定程度上解决了无法获得半数以上的确认,但是在数据同步时会消耗大量的时间降低了redis本身的可用性。

RedisSon高并发分布式锁实战RedisSon源码解读相关推荐

  1. [并发编程] - Executor框架#ThreadPoolExecutor源码解读03

    文章目录 Pre execute源码分析 addWorker()解读 Worker解读 Pre [并发编程] - Executor框架#ThreadPoolExecutor源码解读02 说了一堆结论性 ...

  2. [并发编程] - Executor框架#ThreadPoolExecutor源码解读02

    文章目录 Pre 线程池的具体实现 线程池的创建 参数解读 corePoolSize maximumPoolSize keepAliveTime unit workQueue threadFactor ...

  3. 【高并发】高并发分布式锁架构解密,不是所有的锁都是分布式锁!!

    来自:冰河技术 写在前面 最近,很多小伙伴留言说,在学习高并发编程时,不太明白分布式锁是用来解决什么问题的,还有不少小伙伴甚至连分布式锁是什么都不太明白.明明在生产环境上使用了自己开发的分布式锁,为什 ...

  4. 【高并发】高并发分布式锁架构解密,不是所有的锁都是分布式锁(升级版)!!

    点击上方蓝色"Garnett的Java之路",关注并选择"设为星标" 持之以恒,贵在坚持,每天进步一点点! 写在前面 最近,很多小伙伴留言说,在学习高并发编程时 ...

  5. 朋友问我学习高并发需不需要阅读源码,我是这样分析的!!

    来自:冰河技术 写在前面 最近正在写[高并发专题]的文章,其中,在[高并发专题]中,有不少是分析源码的文章,很多读者留言说阅读源码比较枯燥!问我程序员会使用框架了,会进行CRUD了,是否真的有必要阅读 ...

  6. 多线程高并发编程(8) -- Fork/Join源码分析

    一.概念 Fork/Join就是将一个大任务分解(fork)成许多个独立的小任务,然后多线程并行去处理这些小任务,每个小任务处理完得到结果再进行合并(join)得到最终的结果. 流程:任务继承Recu ...

  7. [并发编程] - Executor框架#ThreadPoolExecutor源码解读01

    文章目录 Pre Thread Java线程与OS线程 生命状态 状态切换 线程池 why use case Advantage Executor框架 ThreadPoolExecutor 源码分析 ...

  8. 《大厂高并发分布式锁从入门到实战》第3讲之Redisson分布式锁

    目录 一.Redisson介绍 二.Redisson中的几种锁 大多数生产环境中,一台redis节点肯定不能满足业务需要,通常会采用多节点redis集群部署,但是redis集群部署条件下可能会产生锁失 ...

  9. Linux IO多路复用之epoll网络编程,高并发的使用例子 (含源码)

    本章节是用基本的Linux基本函数加上epoll调用编写一个完整的服务器和客户端例子,可在Linux上运行,客户端和服务端的功能如下: 客户端从标准输入读入一行,发送到服务端 服务端从网络读取一行,然 ...

最新文章

  1. 使用余弦相似度算法计算文本相似度-数学
  2. php linux 调试工具,内网Visual Studio Code通过XDebug远程调试linux服务器PHP脚本
  3. 10.3 在线学习与 MapReduce-机器学习笔记-斯坦福吴恩达教授
  4. 每天一道LeetCode-----判断二叉树左右两边是否成镜像关系
  5. android Arrays.fill()的使用
  6. 如何把一个运行完好的Kafka搞崩溃
  7. MVC+EF三层+抽象工厂项目搭建
  8. Java 获取linux根目录下的文件夹_Windows支持直接访问Linux子系统文件:你的下一台Linux何必是Linux...
  9. 基于Java学院网页的搜索引擎设计和实现
  10. 详细讲述CV的创作与包装
  11. coddenomicon工具
  12. 【CCF】201903-1小中大
  13. testng 定时构建_10自动化测试_持续集成篇
  14. kali linux 里vim如何使用_Linux vim基本的使用方法
  15. Oracle物化视图的简单使用
  16. System V 共享内存区
  17. python第三项开始每一项都等于前两项的积_Python二十九个常见的脚本汇总!
  18. 华人AI学者大盘点:清华培养了最多的高层次人才,韩家炜、吴恩达论文被引数“登顶”
  19. 局域网vnc远程控制软件,那些超级好用的局域网vnc远程控制软件
  20. xpadder教程:自定义设置游戏手柄的图片

热门文章

  1. JAVA课设作业-实现饭店点菜系统源码
  2. AD5290YRMZ10 adi亚德诺
  3. 微软重组:Windows连根拔除,拥抱云和AI
  4. 追尚系统开发APP平台源码搭建模式
  5. java程序收取qq邮箱的邮件_java 用qq邮箱发邮件示例源码(smtp)
  6. 【SIMULINK】感应电机矢量控制FOC 两种磁链观测器建模
  7. 【C++】有关<左移运算符重载>的总结
  8. CREO图文教程:三维设计案例之三弯管(基准点创建曲线进行扫描)图文教程之详细攻略
  9. 基于Python的面向数据安全治理的数据内容智能发现与分级分类
  10. IU8689+IU5706 单声道100W/立体声60W同步升压+功放IC大功率拉杆音箱应用组合方案