11 Redis之高并发问题(读+写) + 分布式锁 + Redisson的锁

8. 高并发问题

Redis做缓存虽减轻了DBMS的压力,减小了RT(Response Time),但在高并发情况下也是可能会出现各种问题的。

8.1 缓存穿透

当用户访问的数据既不在数据库中也不在缓存中,如id为“-1”的数据或id为特别大不存在的数据, 这时的用户很可能是攻击者,攻击会导致数据库压力过大。就会导致每个用户查询都会“穿透”缓存“直抵”数据库。这种情况就称为缓存穿透。
当高度发的访问请求到达时,缓存穿透不仅增加了响应时间,而且还会引发对DBMS的高并发查询,这种高并发查询很可能会导致DBMS的崩溃(对DBMS做的负载均衡暂且不提)。

缓存穿透产生的主要原因有两个:

  • 一是在数据库中没有相应的查询结果,
  • 二是查询结果为空时,不对查询结果进行缓存。

所以,针对以上两点,解决方案也有两个:

  • 对非法请求进行限制,例如限制查询的范围
    a. 接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
    b. 使用布隆过滤器,需要安装redis组件
    c. 使用布谷鸟滤器,布谷鸟过滤器是布隆过滤器的升级版,需要安装redis组件

  • 对数据库中查询结果也为空的查询给出默认值, 并且把这个键值对的缓存有效时间可以设置短一些,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击

8.2 缓存击穿

关键词:定点打击

试想如果所有请求对着一个 key 照死里搞,这是不是就是一种定点打击呢?

怎么理解呢?举个极端的例子:比如某某明星爆出一个惊天狠料,海量吃瓜群众同时访问微博去查看该八卦新闻,而微博 Redis 集群中数据在此刻正好过期了,那么无数的请求则直接打到了微博系统的物理 DB 上,DB 瞬间挂了。

缓存击穿指的就是某key长期有大量请求,但某一瞬间却过期了,那么程序在redis找不到数据,就会去数据库里查询,数据库处理大量的请求的同时导致压力瞬间增大,甚至导致崩溃.
这种情况称为缓存击穿,而该缓存数据称为热点数据。

解决方案:

  • 设置key值永不过期
  • 将key的过期时间设为随机
  • 使用布隆过滤器或者布谷鸟过滤器
  • 使用分布式锁,当多个key过期时,同一时间只有一个查询请求下发到数据库,其他的key等待一个个地轮流查,就可以避免数据库压力过大的问题;代码如下:
   // 分布式锁,为了可读性高用 ReentrantLock 代替分布式锁
    static Lock lock = new ReentrantLock();
    
    public String getData(String key ) throws InterruptedException {
        try {
            // 从redis获取值
            String data =  getRedisData(key);
            // 如果key不存在,从数据库查询
            if(null  == data){
                // 尝试获取锁
                if(!lock.tryLock()){
                   // 获取锁失败 ,100ms后在次尝试
                    TimeUnit.MILLISECONDS.sleep(100);
                    data = getData(key);
                }
                // 走到这里表示成功获取锁

                // 从myqsl中获取锁
                data = getMysqlData(key);

                // 将数据更新到redis
                setDataToRedis(key,value);
            }
            return data;
        } catch (Exception e){
            e.printStackTrace();
            throw e;
        } finally {
            // 解锁
            lock.unlock();
        }
    }
  • 双重检测锁机制

8.3.1 穿透和击穿的区别

关于穿透和击穿的区别上面已经介绍的很清楚了,这里在做个总结

  • 穿透 :大量请求了缓存和数据库中都没有的数据,每次都查询数据库,导致数据库压力过大
  • 击穿 : 热点key在同一时间过期,导致所有请求都达到数据库,导致数据库压力过大

8.3 缓存雪崩

关键词:Redis 崩了,没有数据了

这里的 Redis 崩了指的并不是 Redis 集群宕机了。而是说在某个时刻 Redis 集群中的热点 key 都失效了。
如果集群中的热点 key 在某一时刻同时失效了的话,试想海量的请求都将直接打到 DB 上,DB 可能在瞬间就被打爆了,一旦DB崩了,它所带来的连锁反应是可怕的,数据库不可用的情况下你的服务器也无法使用;这就是雪崩效应。

对于缓存雪崩没有很直接的解决方案,最好的解决方案就是预防,即提前规划好缓存的过期时间。要么就是让缓存永久有效,当 DB 中数据发生变化时清除相应的缓存。

如果 DBMS采用的是分布式部署,则将热点数据均匀分布在不同数据库节点中,将可能到来的访问负载均衡开来。

8.4 数据库缓存双写不一致

以上三种情况都是针对高并发读场景中可能会出现的问题,

而在高并发写场景下 , 则可能出现数据库缓存双写不一致的问题

对于数据库缓存双写不一致问题,又分为两种

8.4.1 “修改 DB 并更新缓存”场景

若多个请求要对 DBMS 中同一个数据进行修改,修改后还需要更新缓存中相关数据,
那么程序的异步执行可能会导致缓存与数据库中数据不一致的情况
在这里插入图片描述

8.4.2 “修改 DB 并删除缓存”场景

若两个请求对 DBMS 中同一个数据的操作既包含写也包含读,
且修改后还要删除缓存中相关数据,那么程序的异步执行就可能导致缓存与数据库中数据不一致的情况。

在很多系统中是没有缓存预热 warmup 功能的,为了保持缓存与数据库数据的一致性,一般都是在对数据库执行了写操作后,就会删除相应缓存。

在这里插入图片描述

8.4.3 解决方案

8.4.3.1 延迟双删

延迟双删方案是专门针对于“修改 DB 并删除缓存”场景的解决方案。但该方案并不能彻底解决数据不一致的状况,其只可能降低发生数据不一致的概率。

延迟双删方案是指,在写操作完毕后会立即执行一次缓存的删除操作,然后再停上一段时间(一般为几秒)后再进行一次删除。而两次删除中间的间隔时长,要大于一次缓存写操作
在这里插入图片描述

8.4.3.2 队列

以上两种场景中,只所以会出现数据库与缓存中数据不一致,主要是因为对请求的处理出现了并行。

只要将请求写入到一个统一的队列,只有处理完一个请求后才可处理下一个请求,即让系统对用户请求的处理串行化,就可以完全解决数据不一致的问题。

例如使用ZooKeeper或分布式消息队列MQ

8.4.3.3 分布式锁

使用队列的串行化虽然可以解决数据库与缓存中数据不一致,但系统失去了并发性,降低了性能。

使用分布式锁可以在不影响并发性的前提下,协调各处理线程间的关系,使数据库与缓存中的数据达成一致性。

只需要对数据库中的这个共享数据的访问通过分布式锁来协调对其的操作访问即可


9. 分布式锁

在分布式环境下, 分布式锁大部分是由Lua实现的

9.1 分布式锁的工作原理

当有多个线程要访问某一个共享资源(DBMS 中的数据或 Redis 中的数据,或共享文件等)时,为了达到协调多个线程的同步访问,此时就需要使用分布式锁了。

为了达到同步访问的目的,规定,让这些线程在访问共享资源之前先要获取到一个令牌token,只有具有令牌的线程才可以访问共享资源。这个令牌就是通过各种技术实现的分布式锁。而这个分布锁是一种“互斥资源”,即只有一个。只要有线程抢到了锁,那么其它线程只能等待,直到锁被释放或等待超时。

9.2 场景

某电商平台要对商品 sk:0008 进行秒杀销售。假设参与秒杀的商品数量amount 为 1000 台,每个账户只允许抢购一台,即每个请求只会减少一台库存

9.2.1 SB实现

准备

  1. 添加spring-boot-starter-redis/web依赖

9.2.2 原始版本示例

这里仅编写一个controller

@RestController
public class Seckillcontroller {
	@Autowired
	StringRedisTemplate srt;

	@GetMapping("/sk")
	public string seckillHandler(){
		String stock =srt.opsForValue().get("sk:0008");
		int amount =stock == null ?0 : Integer.parseInt(stock);
		if(amount>0){
			srt.opsForValue().set("sk:0008",String.value0f(--amount));
			return "库存剩余"+ amount +"台";
		}
		return"抱歉,您没抢到";
}
9.2.3 第一个问题: 高并发下的幻读

如果参与秒杀的用户数量特别巨大,那么一定会存在很多用户同时读取 Redis 缓存中的 sk:0008 这个 key,那么大家读取到的 value 很可能是相同的,均大于零,均可购买。此时就会出现“超卖”。

解决思路 : 使用 Redis 实现的分布式锁。

该实现方式主要是通过 SETNX 命令完成的。其基本原理是,SETNX 只有在指定 key 不存在时才能执行成功,分布式系统中的哪个节点抢先成功执行了 SETNX ,谁就抢到了锁,谁就拥有了对共享资源的操作权限。
与此同时,其它节点只能等待锁的释放。一旦拥有锁的节点对共享资源操作完毕,其就可以主动删除该 key,即释放锁。然后其它节点就可重新使用 SETNX 命令抢注该 key,即抢注锁

新建一个SeckillController类:

@RestController
public class SeckillController {
    // 分布式锁的key
    public static final String REDIS_LOCK = "redis_lock";

    @Autowired
    private StringRedisTemplate srt;

    @Value("${server.port}")
    private String serverPort;

    @GetMapping("/sk")
    public String seckillHandler() {

        String result = "抱歉,您没抢到";

        //仅当
        try {
            //setIfAbsent实质就是SETNX, 仅当原键不存在时才能设置
            //我们只是在类中定义了Redis_Lock, 缓存中实际并不存在, 因此第一个执行该逻辑的请求一定能正确设置key
            Boolean lockOK = srt.opsForValue().setIfAbsent(REDIS_LOCK, "I'm a Lock");
            if (!lockOK) {
                return "没抢到锁哟";
            }

            String stock = srt.opsForValue().get("sk:0008");
            //如果stock为null, 即缓存中没获取到, 就将amount设为0, 宣告购买失败
            int amount = stock == null ? 0 : Integer.parseInt(stock);
            //因为每个人只买一件, 如果库存大于0, 则肯定能买到, 故将amount-1后写回缓存
            if (amount > 0) {
                srt.opsForValue().set("sk:0008", String.valueOf(--amount));
                result = "库存剩余" + amount + "台";
                System.out.println(result);
            }
            //无论try的执行结果如何, finally都会执行删除锁,确保其他请求可以执行秒杀逻辑
        } finally {
            srt.delete(REDIS_LOCK);
        }
        return result + "。server is " + serverPort;
    }
}

9.2.4 第二个问题: 锁不会过期

若处理当前请求的 APP 节点主机在执行完“添加锁”语句后突然宕机,其 finally 中的释放锁代码根本就没有执行,那么,其它客户端通过其它 APP 节点主机申请资源时,将会由于无法获得到锁而永久性阻塞。

因此应修改逻辑, 即使获得锁, 也会在一定时间内过期. 两种方法

  • 用 expire 命令为 key 指定过期时间,
  • 在 setnx 命令中直接给出该 key 的过期时间。

第一种方式中 setnx 与 expire 命令是分别执行的,不具备原子性,仍然可能会出现问题。
而第二种方式则是直接在 setnx 中完成了两步操作,具有原子性。

故,应采用第二种方式。

修改seckillHandler方法为 :

	Boolean lockOK = srt.opsForValue().setIfAbsent(REDIS_LOcK, "I'm a Lock", 5, TimeUnit.SECONDS);

9.2.5 第三个问题: 删了别人的锁

在前述例子中, 我们将锁定义为一个唯一的常量, 即只有一把锁

 // 分布式锁的key
    public static final String REDIS_LOCK = "redis_lock";

假设一个场景:
seckillHandler()方法需要调用别的微服务, 且在调用过程中会挂起等待.
某一次请求a的执行seckillHandler方法过程中, 由于调用别的微服务出现意外, 请求a在等待中超过了 5 秒(假设 6 秒),a的锁在缓存中自动过期了。

由于锁已过期,另一个请求 b 就可以通过 setnx 申请到锁 , 并开始执行

到了第 6 秒, 请求 a 得到了别的微服务的响应,回来继续执行程序,一路向下执行到finally程序块, 把锁删除了, 需要注意此时删除的已经不是a自己的锁了, 而是b的锁, b是正常执行, 但锁被删了以后其他人也可以申请锁了, 系统可能出错.

解决思路: 为锁添加标识, 谁申请的锁, 只能由谁主动删除

具体来说,为每个申请锁的客户端随机生成一个 UUID,使用这个 UUID 作为该客户端标识,然后将该 UUID 作为该客户端申请到的锁的 value。在删除锁时,只有在发起当前删除操作的客户端的 UUID 与锁的 value 相同时才可以。

@GetMapping("/sk")
public String seckillHandler() {
	String result ="抱歉,您没抢到";
	//生成UUID
	String uuid =uuID.randomUUID().tostring();
	
	try {
		//将uuid作为值写入锁
		Boolean lockOK = srt.opsForValue().setIfAbsent(REDIS_LOCK, uuid, 5, TimeUnit.SECONDS);
		if(!lockOK){
			return "没抢到锁哟";
			}	
			String stock =srt.opsForValue().get("sk:0008");
			int amount = stock == null ? 0 : Integer.parseInt(stock);
			if(amount > 0) {
      		// 修改数据后再写入Redis
      		srt.opsForValue().set("sk:0008",String.valueOf(--amount));
      		result ="库存剩余"+ amount +"台";
      		System.out.println(result):
      		}
} finally {
		//确认是同一把锁, 删除
		if(srt.opsForValue().get(REDIS_LOCK).equals(uuid)) {
			 srt.deLete(REDIS_LOCK);
		}
}
	return result +"。server is "+ serverPort;
}

9.2.5 第四个问题: 鉴锁与解锁操作不具有原子性

对于finally中代码, 显然不具有原子性.
举个例子, A执行完鉴锁 ,且验证成功, 但此时刚好A的锁时间到了, 缓存中自动删除了, 在A执行第二条语句手动删除之前, B程序获得了锁, 然后A执行了第二条语句手动删除, 此时就把B的锁删掉了.

这种情况下标识并没有避免锁被误删, 根源是鉴锁与解锁操作不具有原子性

		//确认是同一把锁, 删除
		if(srt.opsForValue().get(REDIS_LOCK).equals(uuid)) {
			 srt.deLete(REDIS_LOCK);
		}

解决思路 : 运用Lua脚本 / 开启事务
现实是, 不存在那么那么一条命令, 可以将鉴锁和解锁合为一体, 因此只能自己编写一个脚本.
在脚本中中实现鉴锁和解锁, 执行脚本就是一条命令, 具有天然的原子性

9.2.5 第五个问题: 锁过期且未执行完毕时, 程序仍会继续执行, 引入Redisson可重入锁Reentrant Lock

请求 a 的锁过期,但其业务还未执行完毕;请求 b 申请到了锁,其也正在处理业务。如果此时两个请求都同时修改了共享的库存数据,那么就又会出现数据不一致的问题。

两种解决思路:

  • 增加逻辑 , 使得锁过期之后程序自动返回. 但不太合适, 如果进程不是死锁, 而是正常的超时, 应当给予机会继续执行下去.
  • 给锁续约
    即,在当前业务进程开始执行时,fork 出一个子进程,用于启动一个定时任务。
    该定时任务的定时时间小于锁的过期时间,其会定时查看处理当前请求的业务进程的锁是否已被删除。
    如果已被删除,则子进程结束;如果未被删除,说明当前请求的业务还未处理完毕,则将锁的时间重新设置为“原过期时间”。这种方式称为锁续约,也称为锁续命。

使用 Redisson 的可重入锁可以解决上述问题。

可重入锁是指某个线程已经获得某个锁,可以再次获取锁而不会出现死锁

Redisson 内部使用 Lua 脚本实现了可重入锁, 并且配套有对可重入锁的添加、重入、续约(续命)、释放操作。
Redisson需要用户为锁指定一个 key,但无需为锁指定过期时间,因为它有默认过期时间(当然,也可指定)。
由于该锁具有“可重入”功能,所以 Redisson 会为该锁生成一个计数器,记录一个线程重入锁的次数。每次重入锁(锁续约), 计数器都会加一, 每次锁到期删除, 计数器都会减一, 只有计数器归0, 才算完全释放锁.

9.2.5.1 导入依赖

若要使用 Redisson,必须要导入相应依赖。

<dependency>
 <groupId>org.redisson</groupId>
 <artifactId>redisson</artifactId>
 <version>3.17.6</version>
</dependency>
9.2.5.2 启动类中增加Redisson的Bean

启动类中增加Redisson的Bean, 通过单一Redis主机构造一个Redisson对象

@Value("${spring.redis.host}")
private String redisHost;
@Value("${spring.redis.port}")
private Integer redisPort;

public static void main(string[] args) {
		SpringApplication.run(Application.class,args);
}

		@Bean
		public Redisson redisson() {
		Config config = new Config();
		config.useSingleServer()
					.setAddress(redisHost +":"+ redisPort)
					.setDatabase(0);

		return(Redisson)Redisson.create(config);
	}
9.2.5.3 修改controller方法

redisson封装了几乎所有锁的细节, 需要做的就是指定锁, 获得锁, 解锁

//使用Redisson依然需要自己先定义锁
public static final String REDIS_LOCK = "redis_lock";

@GetMapping("/sk")
public string seckillHandler() {
		String result="抱歉,您没抢到";
		
		//给redisson指定的要用的锁
		RLock rLock = redisson.getLock(REDIS_LOCK);
		try {
				/*尝试获得指定的锁
				最多等待10秒,上锁以后10秒自动解锁,返回true表示加锁成功*/
				boolean lockOK =rLock.tryLock(10,10, TimeUnit.SECOND)
				if(!lockOK) {
						return "没抢到锁哟";
				}
				String stock = srt.opsForValue().get("sk:0008");
				int amount =stock == null ? 0 : Integer.parseInt(stock);
				if(amount>0) {
						srt.opsForValue().set("sk:0008",String.value0f(--amount));
						result ="库存剩余"+ amount +"台";
						System.out.println(result);
				}
		} catch(Exception e) {
				System.out.println("加锁异常:"+e.toString());
				} finally {
						//解锁
						rLock.unlock();
				}
				return result +"。server is "+ serverPort;
		}

9.2.5 第六个问题: 主从集群主从交替的过程中存在锁丢失的问题, 引入redisson 红锁

在 Redis 单机情况下,以上代码是没有问题的。但如果是在 Redis 主从集群中,那么其还存在锁丢失问题。

在 Redis 主从集群中,假设节点 A 为 master,节点 B、C 为 slave。
如果一个请求 a 在处理时申请锁,即向节点 A 添加一个 key。当master节点 A 收到请求后写入 key 成功,然后会立即向处理 a 请求的应用服务器 Sa 响应,然后会向两个 slave 同步该 key。
不过,在同步还未开始时,master节点 A 宕机,节点 B 晋升为 master。此时正好有一个请求 b 申请锁,由于节点 B 中并没有该 key,所以该 key 写入成功,然后会立即向处理 b 请求的应用服务器 Sb 响应。由于 Sa 与Sb 都收到了 key 写入成功的响应,所以它们都可同时对共享数据进行处理。这就又出现了并发问题。

只所以新的 master 节点 B 同意请求 b 的锁申请,是因为主从集群丢失了请求 a 的锁申请,即对于节点 B 来说,其根本就不知道有过请求 a 的锁申请。

解决思路: 仅仅将key写入master节点还不算获得锁, 必须将这个key同步到多数slave节点后, 才允许进程获得锁.

Redisson 红锁要求,必须要构建出至少三个 Redis 主从集群。若一个请求要申请锁,必须向所有主从集群中提交 key 写入请求,只有
当大多数集群锁写入成功后,该锁才算申请成功。

9.2.5.1 启动类创建三个redissonBean

本例中我们使用一主二从的配置, 相应地, 要在启动类中创建三个对应的 redissonBean

@Bean("redisson-1")
public Redisson redisson1(){
		Config config = new Config();
		config.useSentinelServers()
				  .setMasterName("mymaster1")
					.addSentinelAddress("redis:16380","redis:16381","redis:16382"); 
		return (Redisson) Redisson.create(config);

@Bean("redisson-2")
public Redisson redisson2() {
		Config config = new Config();
		config.useSentinelServers()
				  .setMasterName("mymaster2")
					.addSentinelAddress("redis:26380","redis:26381","redis:26382")
		return (Redisson) Redisson.creat6e(config):

@Bean("redisson-3")
public Redisson redisson3() {
		Config config =new Config();
		config.useSentinelServers()
		  		.setMasterName("mymaster3")
					.addSentinelAddress("redis:36380","redis:36381" "redis:36382");
		return (Redisson) Redisson.create(config);
9.2.5.2 controller
//使用Redisson依然需要自己先定义锁
public static final String REDIS_LOCK = "redis_lock";

@GetMapping("/sk")
public string seckillHandler() {
		String result="抱歉,您没抢到";
//使用byname方式注入
@Resource(name ="redisson-1")
private Redisson redisson1;
@Resource(name ="redisson-2")
private Redisson redisson2;
@Resource(name ="redisson-3")
private Redisson redisson3;

//seckillController方法中只要分别指定三个锁, 并用这三个锁获取红锁即可, 其他操作不变
RLock rLock1 = redisson1.getLocK(REDIS_LOCK + "_1");
RLock rLock2 = redisson2.getLock(REDIS_LOCK + "_2");
RLock rLock3 = redisson3.getLock(REDIS_LOCK + "_3");

RLock rLock = new RedissonRedLock(rLock1,rLock2,rLock3);
try {
				/*尝试获得指定的锁
				最多等待10秒,上锁以后10秒自动解锁,返回true表示加锁成功*/
				boolean lockOK =rLock.tryLock(10,10, TimeUnit.SECOND)
				if(!lockOK) {
						return "没抢到锁哟";
				}
				String stock = srt.opsForValue().get("sk:0008");
				int amount =stock == null ? 0 : Integer.parseInt(stock);
				if(amount>0) {
						srt.opsForValue().set("sk:0008",String.value0f(--amount));
						result ="库存剩余"+ amount +"台";
						System.out.println(result);
				}
		} catch(Exception e) {
				System.out.println("加锁异常:"+e.toString());
				} finally {
						//解锁
						rLock.unlock();
				}
				return result +"。server is "+ serverPort;
		}

9.2.5 第七个问题: 只有一把锁 + 请求串行化联合系统性能降低, 引入分段锁

无论前面使用的是哪种锁,它们解决并发问题的思路都是相同的,那就将所有请求通过锁实现串行化。而一件商品又只有一把锁, 如果商品有100件, 即使毫无意外的情况下, 也要串行请求锁加锁解锁100次, 系统性能严重不足.

解决思路: 一把锁不够用就用多把锁.
如果原来有100件商品, 将100件商品拆分10份, 每份10件. 每一份商品都用一把锁守护, 将并发性提高十倍.

对于分段式锁, 主要操作是将商品拆分, 业务层面的改动不多.
在这里插入图片描述


10. Redisson

在这里插入图片描述

Redisson 是一个在 Redis 的基础上实现的 Java 驻内存数据网格(In-Memory Data Grid)。
它不仅提供了一系列的分布式的 Java 常用对象,还提供了许多分布式服务.
但我们主要使用它提供的分布式锁RLock

RLock rLock = new RedissonXXXX(XXXX);

对于分布式锁RLock, 内置了一些方法

    //----------------------RLock接口方法-----------------------
    /**
     * 加锁 上面是默认30秒这里可以手动设置锁的有效时间
     *
     * @param leaseTime 锁有效时间
     * @param unit      时间单位 小时、分、秒、毫秒等
     */
    void lock(long leaseTime, TimeUnit unit);

   /**
     * tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,
     * 在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
     *
     * @param time 等待时间
     * @param unit 时间单位 小时、分、秒、毫秒等
     */
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    
    /**
     * 这里比上面多一个参数,多添加一个锁的有效时间
     *
     * @param waitTime  等待时间
     * @param leaseTime 锁有效时间
     * @param unit      时间单位 小时、分、秒、毫秒等
     */
    boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException;
    
    /**
     * 检验该锁是否被线程使用,如果被使用返回True
     */
    boolean isLocked();
    
    /**
     * 检查当前线程是否获得此锁(可以判断是否当前线程获得此锁,而不是此锁是否被线程占有)
     */
    boolean isHeldByCurrentThread();
    
    /**
     * 中断锁 和上面中断锁差不多,只是这里如果获得锁成功,添加锁的有效时间
     * @param leaseTime  锁有效时间
     * @param unit       时间单位 小时、分、秒、毫秒等
     */
    void lockInterruptibly(long leaseTime, TimeUnit unit);  
}

需要注意的是,为了避免锁到期但业务逻辑没有执行完毕而引发的多个线程同时访问共享资源的情况发生,Redisson 内部为锁提供了一个监控锁的看门狗 watch dog,其会在锁到期前不断延长锁的到期时间,直到锁被主动释放。即会自动完成“锁续命”。

10.1 可重入锁 ReentrantLock

Redisson 的分布式锁 RLock 是一种可重入锁。当一个线程获取到锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的。

  • JDK 中的 ReentrantLock 是可重入锁,其是通过 AQS(抽象队列同步器)实现的锁机制
  • synchronized 也是可重入锁,其是通过监视器模式(本质是 OS 的互斥锁)实现的锁机制

10.2 公平锁 FairLock

Redisson 的可重入锁 RLock 默认是一种非公平锁,但也支持可重入公平锁 FailLock。当有多个线程同时申请锁时,这些线程会进入到一个 FIFO 队列,只有队首元素才会获取到锁,其它元素等待。只有当锁被释放后,才会再将锁分配给当前的队首元素

RLock fairLock = redisson.getFairLock();

10.3 联锁 MultiLock

当一个线程需要同时处理多个共享资源时,可使用联锁。即一次性申请多个锁,同时锁定多个共享资源。
联锁可预防死锁。
相当于对共享资源的申请实现了原子性:要么都申请到,只要缺少一个资源,则将申请到的所有资源全部释放。
其是 OS 底层原理中 AND 型信号量机制的典型应用

RLock rLock1 = redisson1.getLocK(REDIS_LOCK + "_1");
RLock rLock2 = redisson2.getLock(REDIS_LOCK + "_2");
RLock rLock3 = redisson3.getLock(REDIS_LOCK + "_3");

RLock rLock = new RedissonMultiLock(rLock1,rLock2,rLock3);
	
//如果将Redisson纳入了Spring管理, 便可以直接获取
RLock rLock = reddison.getMultiLock(rLock1,rLock2,rLock3);

10.4 红锁 RedLock

红锁一般用于解决 Redis 主从集群锁丢失问题。

红锁由多个锁构成,只有当这些锁中的大部分锁申请成功时,红锁才申请成功。

红锁与联锁的区别是,红锁实现的是对一个共享资源的同步访问控制,而联锁实现的是多个共享资源的同步访问控制。

RLock rLock1 = redisson1.getLocK(REDIS_LOCK + "_1");
RLock rLock2 = redisson2.getLock(REDIS_LOCK + "_2");
RLock rLock3 = redisson3.getLock(REDIS_LOCK + "_3");

RLock rLock = new RedissonRedLock(rLock1,rLock2,rLock3);

10.5 读写锁RReadWriteLock

通过 RReadWriteLock 实例可分别获取到读锁 RedissonReadLock 与写锁 RedissonWriteLock。
读锁与写锁分别是实现了 RLock 的可重入锁。
一个共享资源,在没有写锁的情况下,允许同时添加多个读锁。只要添加了写锁,任何读锁与写锁都不能再次添加。即读锁是共享锁,写锁为排他锁。

10.6 信号量RSaphomore

10.7 可过期信号量PermitExpirableSemaphore

10.8 分布式闭锁RCountDownLatch


  • 17
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值