首页 > 数据库 >#yyds干货盘点#Redis分布式锁正确打开方式

#yyds干货盘点#Redis分布式锁正确打开方式

时间:2023-08-04 10:06:27浏览次数:40  
标签:yyds 逻辑 lock Redis try finally 线程 打开方式 分布式

1、为什么要有分布式锁?

JUC提供的锁机制,可以保证在同一个JVM进程中同一时刻只有一个线程执行操作逻辑;

多服务多节点的情况下,就意味着有多个JVM进程,要做到这样,就需要有一个中间人;

分布式锁就是用来保证在同一时刻,仅有一个JVM进程中的一个线程在执行操作逻辑;

换句话说,JUC的锁和分布式锁都是一种保护系统资源的措施。尽可能将并发带来的不确定性转换为同步的确定性;


2、分布式锁特性(五大特性 非常重要)

特性1:互斥性。在任意时刻,只有一个客户端能持有锁。

特性2: 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。

特性3: 解铃还须系铃人。加锁和解锁必须是同一个客户端(线程),客户端自己不能把别人加的锁给解了。

特性4:可重入性。同一个现线程已经获取到锁,可再次获取到锁。

特性5: 具有容错性。只要大部分的分布式锁节点正常运行,客户端就可以加锁和解锁。

2-1 常见分布式锁的三种实现方式

1. 数据库锁;2. 基于ZooKeeper的分布式锁;3. 基于Redis的分布式锁。

2-2 本文我们主要聊 redis实现分布式锁:

一个 setnx 就行了?value没意义?还有人认为 incr 也可以?再加个超时时间就行了?

3、分布式锁特性2之不会发生死锁

很多线程去上锁,谁锁成功谁就有权利执行操作逻辑,其他线程要么直接走抢锁失败的逻辑,要么自旋尝试抢锁;

• 比方说 A线程竞争到了锁,开始执行操作逻辑(代码逻辑演示中,使用 Jedis客户端为例);

public static void doSomething() {
    // RedisLock是封装好的一个类
    RedisLock redisLock = new RedisLock(jedis); // 创建jedis实例的代码省略,不是重点
    try {
        redisLock.lock(); // 上锁
        
        // 处理业务
        System.out.println(Thread.currentThread().getName() + " 线程处理业务逻辑中...");
        Thread.sleep(2000);
        System.out.println(Thread.currentThread().getName() + " 线程处理业务逻辑完毕");
        
        redisLock.unlock(); // 释放锁
    } catch (Exception e) {
        e.printStackTrace();
    }
}

• 正常情况下,A 线程执行完操作逻辑后,应该将锁释放。如果说执行过程中抛出异常,程序不再继续走正常的释放锁流程,没有释放锁怎么办?所以我们想到:

• 释放锁的流程一定要在 finally{} 块中执行,当然,上锁的流程一定要在 finally{} 对应的 try{} 块中,否则 finally{} 就没用了,如下:

public static void doSomething() {
    RedisLock redisLock = new RedisLock(jedis); // 创建jedis实例的代码省略,不是重点
    try {
        redisLock.lock(); // 上锁,必须在 try{}中
        
        // 处理业务
        System.out.println(Thread.currentThread().getName() + " 线程处理业务逻辑中...");
        Thread.sleep(2000);
        System.out.println(Thread.currentThread().getName() + " 线程处理业务逻辑完毕");
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        redisLock.unlock(); // 在finally{} 中释放锁
    }
}

写法注意: redisLock.lock(); 上分布式锁,必须在 try{}中。

在JAVA多线程中 lock.lock(); 单机多线程加锁操作需要在try{}之前。

3-1 redisLock.unlock() 放在 finally{} 块中就行了吗?还需要设置超时时间

如果在执行 try{} 中逻辑的时候,程序出现了 System.exit(0); 或者 finally{} 中执行异常,比方说连接不上 redis-server了;或者还未执行到 finally{}的时候,JVM进程挂掉了,服务宕机;这些情况都会导致没有成功释放锁,别的线程一直拿不到锁,怎么办?如果我的系统因为一个节点影响,别的节点也都无法正常提供服务了,那我的系统也太弱了。所以我们想到必须要将风险降低,可以给锁设置一个超时时间,比方说 1秒,即便发生了上边的情况,那我的锁也会在 1秒之后自动释放,其他线程就可以获取到锁,接班干活了;

public static final String lock_key = "zjt-lock";
 
     public void lock() {        
        while (!tryLock()) {
            try {
                Thread.sleep(50); // 在while中自旋,如果说读者想设置一些自旋次数,等待最大时长等自己去扩展,不是此处的重点
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        
        System.out.println("线程:" + threadName + ",占锁成功!★★★");
     }
 
      private boolean tryLock() {
        SetParams setParams = new SetParams();
        setParams.ex(1); // 超时时间1s
        setParams.nx();  // nx
        String response = jedis.set(lock_key, "", setParams); // 转换为redis命令就是:set zjt-key "" ex 1 nx
        return "OK".equals(response);
     }

注意,上锁的时候,设置key和设置超时时间这两个操作要是原子性的,要么都执行,要么都不执行。

Redis原生支持:

// http://redis.io/commands/set.html
SET key value [EX seconds] [PX milliseconds] [NX|XX]

不要在代码里边分两次调用:

set k v
exipre k time

3-2 锁的超时时间该怎么计算?

刚才假设的超时时间 1s是怎么计算的?这个时间该设多少合适呢?

锁中的业务逻辑的执行时间,一般是我们在测试环境进行多次测试,然后在压测环境多轮压测之后,比方说计算出平均的执行时间是 200ms,锁的超时时间放大3-5倍,比如这里我们设置为 1s,为啥要放大,因为如果锁的操作逻辑中有网络 IO操作,线上的网络不会总一帆风顺,我们要给网络抖动留有缓冲时间。另外,如果你设置 10s,果真发生了宕机,那意味着这 10s中间,你的这个分布式锁的服务全部节点都是不可用的,这个和你的业务以及系统的可用性有挂钩,要衡量,要慎重(后边3-13会再详细聊)。那如果一个节点宕机之后可以通知 redis-server释放锁吗?注意,我是宕机,不可控力,断电了兄弟,通知不了的。

回头一想,如果我是优雅停机呢,我不是 kill -9,也不是断电,这样似乎可以去做一些编码去释放锁,你可以参考下 JVM的钩子、Dubbo的优雅停机、或者 linux进程级通信技术来做这件事情。当然也可以手动停服务后,手动删除掉 redis中的锁。

标签:yyds,逻辑,lock,Redis,try,finally,线程,打开方式,分布式
From: https://blog.51cto.com/u_11365839/6957171

相关文章

  • Redis从入门到放弃(8):哨兵模式
    在前面的文章中介绍了Redis的主从复制,但主从复制存在一定的缺陷。如果Master节点宕机,因为不具备自动恢复功能,需要人工干预,那么在这个干预过程中Redis将不可用。为了解决这一问题,Redis官方推荐一种高可用方案:哨兵模式(Sentinel)。1、什么是哨兵模式?哨兵模式是Redis的高可用解决方......
  • pyspark 环境搭建和相关操作redis ,es
    一.环境搭建1.创建虚拟环境,指定python包2.切换到虚拟环境,安装你所需要的python相关模块包3.把整个虚拟环境打成.zip4.将zip上传的hadfs5.spark-submit指定python包的路径可以参考 https://dandelioncloud.cn/article/details/1589470996832964609二.pyspark数据r......
  • Redis持久化
    RDB持久化Redis可以通过创建快照来获得存储在内存里面的数据在 某个时间点 上的副本。Redis创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis主从结构,主要用来提高Redis性能),还可以将快照留在原地以便重启服务器的时候使......
  • A07、redis
    review#Spring全家桶#Spring#中间件回顾1.springboot特性版本锁定继承官方的parent起步依赖完成某个功能的坐标集合体自动装配可以通过默认规则创建对象2.yml配置文件简单对象/map集合3.整合junit4.整合ssm5.整合日志今日目标1.掌握......
  • Redis从入门到放弃(6):持久化
    文章目录1、引言2、RDB持久化2.1、手动触发2.2、自动触发3、AOF持久化3.1、触发方式3.2、AOF重写4、混合持久化5、RDB与AOF的优缺点对比5.1、RDB5.2、AOF1、引言Redis作为一种高性能的内存数据存储系统,常被用作缓存、会话存储、消息队列等多种应用场景。然而,由于其数据存储在内......
  • Redis从入门到放弃(5):事务
    文章目录1、事务的定义2、事务命令3、事务错误处理4、事务的冲突问题4.1、悲观锁(PessimisticLock)4.2、乐观锁(OptimisticLocking)5、总结:事务三特性1、事务的定义Redis的事务提供了一种“将多个命令打包,然后一次性、按顺序地执行”的机制。redis事务的主要作用就是串联多个命令......
  • Redis从入门到放弃(4):3种新数据类型
    文章目录1、介绍2、Bitmaps(位图)2.1、特性2.2、原理2.3、应用场景2.4、代码3、HyperLogLog(基数统计)3.1、特性3.2、原理3.3、应用场景3.4、代码4、Geospatial(地理位置)4.1、特性4.2、原理4.3、应用场景4.4、代码5、总结1、介绍前面的文章已经介绍了redis的5种基本数据类型,redis6中......
  • Redis从入门到放弃(3):发布与订阅
    文章目录1、介绍2、如何使用发布订阅2.1、订阅频道2.2、发布消息2.3、取消订阅2.4、模式订阅2.5、取消模式订阅3、使用案例(伪代码)4、Redis的发布订阅与ActiveMQ、RocketMQ区别1、介绍Redis是一个快速、开源的内存数据库,支持多种数据结构,如字符串、哈希、列表、集合、有序集合等。......
  • Redis从入门到放弃(1):安装配置
    文章目录1.介绍2.优势3.安装Redis4.后台运行5.配置Redis5.1查看配置项5.2修改配置项5.3参数说明6.错误解决1.介绍Redis是一个高性能的开源key-value数据库。它被广泛应用于缓存、会话存储、实时分析、消息队列等场景。Redis具有以下三个主要特点:数据持久化:Redis支持......
  • Redis从入门到放弃(2):数据类型
    文章目录1.字符串(String)1.1、介绍1.2、命令示例1.3、注意事项2.哈希(Hash)2.1、介绍2.2、命令示例2.3、注意事项3.列表(List)3.1、介绍3.2、命令示例3.3、注意事项4.集合(Set)4.1、介绍4.2、命令示例5.有序集合(SortedSet)5.1、介绍5.2、命令示例6、总结在Redis中,数据以键值对的形......