Redis7-分布式锁

server/2024/9/23 1:44:29/

目录

基本原理

分布式锁的实现

基于Redis的分布式锁

Redis分布式锁误删

分布式锁的原子性问题

基于Redis的分布式锁优化

Redission概述

Redisson入门

Redisson可重入锁原理

Reddisson锁重试和WatchDog机制

Redisson分布式锁原理

Redission的MultiLock原理

​编辑分布式锁总结


基本原理

分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁

分布式锁需要满足的条件:

  • 可见性:多个线程都能看到相同的结果,注意:这个地方说的可见性并不是并发编程中指的内存可见性,只是说多个进程之间都能感知到变化
  • 互斥:互斥是分布式锁的最基本的条件,使得程序串行执行
  • 高可用:程序不易崩溃,时时刻刻都保证较高的可用性
  • 高性能:由于加锁本身就让性能降低,对于分布式锁本身需要有较高的加锁性能和释放锁性能
  • 安全性

分布式锁的实现

分布式锁的核心是实现多线程之间互斥,常见的三种实现方式:

基于Redis的分布式锁

实现分布式锁时需要实现的两个基本方法:

1.获取锁

  • 互斥:确保只能有一个线程获取锁

  • 非阻塞:尝试一次,成功返回true,失败返回false

2.释放锁

  • 手动释放

  • 超时释放:获取锁时添加一个超时时间

流程: 

代码实现:

需求:定义一个类,实现下面接口,利用Redis实现分布式锁功能

public class SimpleRedisLock implements ILock{private static final String KEY_PREFIX="lock:";@Overridepublic boolean tryLock(long timeoutSec) {//获取线程标示String threadId = Thread.currentThread().getId();//获取锁Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);return Boolean.TRUE.equals(success);}@Overridepublic void unlock() {//通过del删除锁stringRedisTemplate.delete(KEY_PREFIX + name);}
}
    @Overridepublic Result seckillVoucher(Long voucherId) {// 1.查询优惠券SeckillVoucher voucher = seckillVoucherService.getById(voucherId);// 2.判断秒杀是否开始if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {// 尚未开始return Result.fail("秒杀尚未开始!");}// 3.判断秒杀是否已经结束if (voucher.getEndTime().isBefore(LocalDateTime.now())) {// 尚未开始return Result.fail("秒杀已经结束!");}// 4.判断库存是否充足if (voucher.getStock() < 1) {// 库存不足return Result.fail("库存不足!");}Long userId = UserHolder.getUser().getId();//创建锁对象(新增代码)SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);//获取锁对象boolean isLock = lock.tryLock(1200);//加锁失败if (!isLock) {return Result.fail("不允许重复下单");}try {//获取代理对象(事务)IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();return proxy.createVoucherOrder(voucherId);} finally {//释放锁lock.unlock();}}

Redis分布式锁误删

持有锁的线程在锁的内部出现了阻塞,导致它的锁自动释放,这时线程2来尝试获得锁,就拿到了这把锁,然后线程2在持有锁执行过程中,线程1反应过来,继续执行,而线程1执行过程中,走到了删除锁逻辑,此时就会把本应该属于线程2的锁进行删除,这就是误删别人锁的情况说明

解决方案:在每个线程释放锁的时候,去判断一下当前这把锁是否属于自己,如果不属于自己,则不进行锁的删除

需求:

修改之前的分布式锁实现,满足:

1.在获取锁时存入线程标示(可以用UUID表示)

2.在释放锁时先获取锁中的线程标示,判断是否与当前线程标示一致

  • 如果一致则释放锁

  • 如果不一致则不释放锁

代码实现:

获取锁

private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
@Override
public boolean tryLock(long timeoutSec) {// 获取线程标示String threadId = ID_PREFIX + Thread.currentThread().getId();// 获取锁Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);return Boolean.TRUE.equals(success);
}

释放锁

public void unlock() {// 获取线程标示String threadId = ID_PREFIX + Thread.currentThread().getId();// 获取锁中的标示String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);// 判断标示是否一致if(threadId.equals(id)) {// 释放锁stringRedisTemplate.delete(KEY_PREFIX + name);}
}

分布式锁的原子性问题

线程1现在持有锁之后,在执行业务逻辑过程中,正准备删除锁,而且已经走到了条件判断的过程中,比如它已经拿到了当前这把锁,确实是属于自己的,正准备删除锁,但是此时它的锁到期了,那么此时线程2进来,但是线程1它会接着往后执行,当它卡顿结束后,它直接就会执行删除锁那行代码,相当于条件判断并没有起到作用,这就是删锁时的原子性问题

Lua脚本解决多条命令原子性问题

Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。Lua是一种编程语言,它的基本语法:Lua 教程 | 菜鸟教程

Redis提供的调用函数:

redis.call('命令名称', 'key', '其它参数', ...)

例:执行set name jack

# 执行 set name jack
redis.call('set', 'name', 'jack')

例:先执行set name Rose,再执行get name

# 先执行 set name jack
redis.call('set', 'name', 'Rose')
# 再执行 get name
local name = redis.call('get', 'name')
# 返回
return name

写好脚本以后,需要用Redis命令来调用脚本,调用脚本的常见命令如下:

例:执行 redis.call('set', 'name', 'jack') 这个脚本

 

如果脚本中的key、value不想写死,可以作为参数传递。key类型参数会放入KEYS数组,其它参数会放入ARGV数组,在脚本中可以从KEYS和ARGV数组获取这些参数:  

Lua脚本实现释放锁:

-- 这里的 KEYS[1] 就是锁的key,这里的ARGV[1] 就是当前线程标示
-- 获取锁中的标示,判断是否与当前线程标示一致
if (redis.call('GET', KEYS[1]) == ARGV[1]) then-- 一致,则删除锁return redis.call('DEL', KEYS[1])
end
-- 不一致,则直接返回
return 0

利用Java代码调用Lua脚本改造分布式锁

private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;static {UNLOCK_SCRIPT = new DefaultRedisScript<>();UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));UNLOCK_SCRIPT.setResultType(Long.class);}public void unlock() {// 调用lua脚本stringRedisTemplate.execute(UNLOCK_SCRIPT,Collections.singletonList(KEY_PREFIX + name),ID_PREFIX + Thread.currentThread().getId());
}

总结

基于Redis的分布式锁实现思路:

  • 利用set nx ex获取锁,并设置过期时间,保存线程标示

  • 释放锁时先判断线程标示是否与自己一致,一致则删除锁

特性:

  • 利用set nx满足互斥性

  • 利用set ex保证故障时锁依然能释放,避免死锁,提高安全性

  • 利用Redis集群保证高可用和高并发特性

基于Redis的分布式锁优化

基于setnx实现的分布式锁存在下面的问题:

Redission概述

Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid),它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现

Redission提供了分布式锁的多种多样的功能:

官网:https://redisson.org

Redisson入门

1.引入依赖:

<dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.13.6</version>
</dependency>

2.配置Redisson客户端:

@Configuration
public class RedissonConfig {@Beanpublic RedissonClient redissonClient(){// 配置Config config = new Config();config.useSingleServer().setAddress("redis://127.0.0.1:6379");// 创建RedissonClient对象return Redisson.create(config);}
}

3.使用Redission的分布式锁:

@Resource
private RedissionClient redissonClient;@Test
void testRedisson() throws Exception{//获取锁(可重入),指定锁的名称RLock lock = redissonClient.getLock("anyLock");//尝试获取锁,参数分别是:获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位boolean isLock = lock.tryLock(1,10,TimeUnit.SECONDS);//判断获取锁成功if(isLock){try{System.out.println("执行业务");          }finally{//释放锁lock.unlock();}}}

4.在 VoucherOrderServiceImpl注入RedissonClient:

@Resource
private RedissonClient redissonClient;@Override
public Result seckillVoucher(Long voucherId) {// 1.查询优惠券SeckillVoucher voucher = seckillVoucherService.getById(voucherId);// 2.判断秒杀是否开始if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {// 尚未开始return Result.fail("秒杀尚未开始!");}// 3.判断秒杀是否已经结束if (voucher.getEndTime().isBefore(LocalDateTime.now())) {// 尚未开始return Result.fail("秒杀已经结束!");}// 4.判断库存是否充足if (voucher.getStock() < 1) {// 库存不足return Result.fail("库存不足!");}Long userId = UserHolder.getUser().getId();//创建锁对象 使用分布式锁RLock lock = redissonClient.getLock("lock:order:" + userId);//获取锁对象boolean isLock = lock.tryLock();//加锁失败if (!isLock) {return Result.fail("不允许重复下单");}try {//获取代理对象(事务)IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();return proxy.createVoucherOrder(voucherId);} finally {//释放锁lock.unlock();}}

Redisson可重入锁原理

Reddisson锁重试和WatchDog机制

Redisson分布式锁原理

可重入:利用hash结构记录线程id和重入次数

可重试:利用信号量和Pubsub功能实现等待、唤醒,获取锁失败的重试机制

超时续约:利用whtchDog,每隔一段时间(releaseTime / 3),重置超时时间

Redission的MultiLock原理

为了提高redis的可用性,会搭建集群或者主从,以主从为例

此时去写命令,写在主机上, 主机会将数据同步给从机,但是假设主机还没有来得及把数据写入到从机时,此时主机宕机,哨兵会发现主机宕机,并且选举一个slave变成master,而此时新的master中实际上并没有锁信息,锁信息已经丢掉了

为了解决这个问题,Redission提出来了MultiLock锁,使用这把锁就不再使用主从,每个节点的地位都是一样的, 这把锁加锁的逻辑需要写入到每一个主丛节点上,只有所有的服务器都写入成功,此时才是加锁成功,假设现在某个节点挂了,那么它去获得锁的时候,只要有一个节点拿不到,都不能算是加锁成功,就保证了加锁的可靠性

MultiLock加锁原理:

当设置了多个锁时,Redission会将多个锁添加到一个集合中,然后用while循环不停去尝试拿锁,但是会有一个总共的加锁时间,这个时间是用需要加锁的个数 * 1500ms ,假设有3个锁,那么时间就是4500ms,假设在这4500ms内,所有的锁都加锁成功, 那么此时才算是加锁成功,如果在4500ms有线程加锁失败,则会再次去进行重试

分布式锁总结

1.不可重入Redis分布式锁

  • 原理:利用setnx的互斥性;利用ex避免死锁;释放锁时判断线程标示
  • 缺陷:不可重入、无法重试、锁超时失效

2.可重入的Redis分布式锁

  • 原理:利用hash结构,记录线程标示和重入次数;利用watchDog延续锁时间;利用信号量控制锁重试等待
  • 缺陷:Redis宕机引起锁失效问题

3.Redisson的MultiLock

  • 原理:多个独立的Redis节点,必须在所有节点都获取重入锁,才算获取锁成功
  • 缺陷:运维成本高,实现复杂

http://www.ppmy.cn/server/98642.html

相关文章

2024年武汉东湖高新区职称第二批次开始了

众所周知&#xff0c;武汉市东湖高新区职称一年两批次&#xff0c;今年下半年第二批水平能力测试报名也已经开始了&#xff0c;请注意报名时间&#xff0c;别错过&#xff01;&#xff01; 2024年武汉东湖高新区第二批次水测报名时间&#xff1a;&#xff08;一&#xff09;网上…

matlab基础操作(五)

31.数组维数的减小 >> amagic(4),a(:,2)[] >> a(1,2)[] 带有下标的赋值维度不匹配。 >> a(2:4)[]%数组a将变为向量 32.元胞数组的创建 Cell indexing方式创建元胞数组 >> c(1,1){[1 4 3;0 5 8;7 2 9]} >> c(1,2){Anne Smith} >> c(2,1){…

大数据-72 Kafka 高级特性 稳定性-事务 (概念多枯燥) 定义、概览、组、协调器、流程、中止、失败

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; 目前已经更新到了&#xff1a; Hadoop&#xff08;已更完&#xff09;HDFS&#xff08;已更完&#xff09;MapReduce&#xff08;已更完&am…

五、ESP32-S3上使用MicroPython点亮WS2812智能LED灯珠并通过web控制改变灯珠颜色

实现通过ESP32S3连接Wi-Fi并使用Web页面控制WS2812灯珠的颜色&#xff0c;可以使用ESP32的WebServer库来创建一个简单的Web界面。通过这个界面&#xff0c;你可以动态地控制灯珠的显示效果。 主要步骤&#xff1a; 设置Wi-Fi连接: 让ESP32S3连接到你的Wi-Fi网络。创建Web服务…

[MRCTF2020]套娃 php字符串解析绕过,jsfuck编码

进来看到代码 <!-- //1st $query $_SERVER[QUERY_STRING];if( substr_count($query, _) ! 0 || substr_count($query, %5f) ! 0 ){die(Y0u are So cutE!); }if($_GET[b_u_p_t] ! 23333 && preg_match(/^23333$/, $_GET[b_u_p_t])){echo "you are going to th…

SpringBoot自动装配原理

SpringBoot自动装配原理 SpringBootApplication 中包含了三个核心注解 SpringBootConfiguration 声明当前的类是配置类 ComponentScan 组件扫描,默认扫描引导类所在包以及子包 EnableAutoConfiguration 实现SpringBoot自动化配置的核心注解 EnableAutoConfiguration …

SS9283403 sqlite3交叉编译并部署到SS928(六)

1.Sqlite3下载 连接&#xff1a;SQLite Download Page 2.解压 tar zxvf sqlite-autoconf-3460000.tar.gz 3.配置并编译 进入解压目录&#xff0c;打开命令行&#xff0c;输入如下命令 ./configure CCaarch64-mix210-linux-gcc --hostarm-linux --prefix/home/mc/work/sqlite…

excel中有些以文本格式存储的数值如何批量转换为数字

一、背景 1.1 文本格式存储的数值特点 在平时工作中有时候会从别地方导出来表格&#xff0c;表格中有些数值是以文本格式存储的&#xff08;特点&#xff1a;单元格的左上角有个绿色的小标&#xff09;。 1.2 文本格式存储的数值在排序时不符合预期 当我们需要进行排序的时候…