分布式锁
案例搭建
需求背景:
电商项目中,用户购买商品后,会对商品的库存进行扣减
需求实现:
根据用户购买商品及购买商品数量,对商品库存进行指定数量的扣减
-
数据库脚本
create table if not exists `good_stock` ( `id` bigint not null auto_increment, `goods_id` bigint not null, `stock` int not null, primary key (`id`) ) engine = InnoDB auto_increment = 1 default charset = utf8mb4 comment ='商品库存表'; insert into good_stock (goods_id, stock)values (1,1);
-
案例技术点
- Spring Boot3.0
- JDK17
- Mybatis
- Lombok
-
新建distribute-lock-stock项目
pom.xml文件
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.0.6</version> <relativePath/> <!-- lookup parent from repository --> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>3.0.0</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter-test</artifactId> <version>3.0.0</version> <scope>test</scope> </dependency> </dependencies>
-
配置application.yml
server: port: 9099 spring: application: name: stock-application datasource: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://10.11.1.96:3306/shop?serverTimezone=GMT%2B8&autoReconnect=false&useUnicode=true&characterEncoding=UTF-8&characterSetResults=UTF-8&zeroDateTimeBehavior=convertToNull&useSSL=true username: root password: xxxxxx
-
创建mapper接口
package com.ajie.stock.mapper; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import org.apache.ibatis.annotations.Update; /** * @author ajie * @description: TODO */ @Mapper public interface StockMapper { /** * 根据商品id查询库存 * @param goodsId * @return */ @Select("select stock from good_stock where goods_id = #{goodsId}") Integer selectStockByGoodsId(@Param("goodsId") Long goodsId); /** * 根据商品id更新库存 * @param goodsId * @param stock * @return */ @Update("update good_stock set stock = #{stock} where goods_id = #{goodsId};") Integer updateStockByGoodsId(@Param("goodsId") Long goodsId, @Param("stock") Integer stock); }
-
编写service层代码
StockService
package com.ajie.stock.service; public interface StockService { String deductStock(Long goodsId, Integer stock); }
StockServiceImpl
package com.ajie.stock.service.impl; import com.ajie.stock.mapper.StockMapper; import com.ajie.stock.service.StockService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; /** * @author ajie * @description: TODO */ @Service public class StockServiceImpl implements StockService { @Autowired private StockMapper stockMapper; @Override public String deductStock(Long goodsId, Integer count) { // 1.根据商品id查询库存 Integer stock = stockMapper.selectStockByGoodsId(goodsId); // 2.判断库存数量是否足够 if (stock < count) { return "库存不足"; } // 3.库存数量足够,扣减库存 stockMapper.updateStockByGoodsId(goodsId, stock - count); return "库存扣减成功"; } }
-
编写controller层代码
package com.ajie.stock.controller; import com.ajie.stock.service.StockService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * @author ajie * @description: TODO */ @RestController @RequestMapping("stock") public class StockController { @Autowired private StockService stockService; @GetMapping("deductStock/{goodsId}/{count}") public String deductStock(@PathVariable Long goodsId, @PathVariable Integer count){ return stockService.deductStock(goodsId, count); } }
-
启动项目进行测试
在resources目录下新建.http文件stock.http
自此项目案例搭建完毕,库存也成功扣减
jemeter使用
-
创建线程组
-
创建http请求
-
创建聚合报告
-
将数据库中的库存改为6000,因为一共会发送6000次请求。然后点击启动jemeter
结果如下图所示
从结果可以看出,库存量并没有按照预期的结果减为0。这里涉及到了并发操作共享资源的问题
jvm锁
使用synchronized
修改service类中的方法,加上synchronized关键字
@Override
public synchronized String deductStock(Long goodsId, Integer count) {
// 1.根据商品id查询库存
Integer stock = stockMapper.selectStockByGoodsId(goodsId);
// 2.判断库存数量是否足够
if (stock < count) {
return "库存不足";
}
// 3.库存数量足够,扣减库存
stockMapper.updateStockByGoodsId(goodsId, stock - count);
return "库存扣减成功";
}
重新启动项目,修改数据库的库存为6000,使用jemeter测试,进行两次测试,记录第二次的执行性能
通过上面两个截图可以看到,通过加synchronized可以解决库存因为并发扣减异常的问题
使用ReentrantLock
-
修改service类中的方法
public static final ReentrantLock LOCK = new ReentrantLock(); @Override public String deductStock(Long goodsId, Integer count) { // 加锁 LOCK.lock(); try { // 1.根据商品id查询库存 Integer stock = stockMapper.selectStockByGoodsId(goodsId); // 2.判断库存数量是否足够 if (stock < count) { return "库存不足"; } // 3.库存数量足够,扣减库存 stockMapper.updateStockByGoodsId(goodsId, stock - count); return "库存扣减成功"; } finally { // 解锁 LOCK.unlock(); } }
-
重新启动项目,修改数据库的库存为6000,使用jemeter测试,进行两次测试,记录第二次的执行性能
通过上面两个截图可以看到,通过加ReentrantLock也可以解决库存因为并发扣减异常的问题
执行性能对比
三种执行方式的性能对比
## 没有任何限制的情况下
HTTP请求 6000 63 49 115 158 258 7 606 0.0 670.8407871198568 117.76291193817083 91.06139590787119
## 加synchronized锁
HTTP请求 6000 951 865 1811 2207 3019 14 4535 0.0 50.99959200326398 8.964772031823745 6.922796180130559
## 加ReentrantLock锁
HTTP请求 6000 952 876 1463 1704 1975 16 2254 0.0 52.028685148411824 9.145667311244265 7.062487534794183
可以看到加锁以后有明显的吞吐量下降
jvm锁失效场景
多例模式
在多例模式下,锁失效必须满足以下条件:
- 使用synchronized锁
- 类的代理使用CGLIB代理
- synchronized锁的对象必须是某个类的实例对象,而不能是类对象
案例演示:
-
修改service代码
package com.ajie.stock.service.impl; import com.ajie.stock.mapper.StockMapper; import com.ajie.stock.service.StockService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Scope; import org.springframework.context.annotation.ScopedProxyMode; import org.springframework.stereotype.Service; /** * @author ajie * @description: TODO */ @Service // 使用scope注解,将Spring的bean对象变成多例的 @Scope(value = "prototype",proxyMode = ScopedProxyMode.TARGET_CLASS) public class StockServiceImpl implements StockService { @Autowired private StockMapper stockMapper; @Override public synchronized String deductStock(Long goodsId, Integer count) { // 1.根据商品id查询库存 Integer stock = stockMapper.selectStockByGoodsId(goodsId); // 2.判断库存数量是否足够 if (stock < count) { return "库存不足"; } // 3.库存数量足够,扣减库存 stockMapper.updateStockByGoodsId(goodsId, stock - count); return "库存扣减成功"; } }
-
修改数据中的库存,进行测试
可以看出,使用多例模式锁失效了
锁失效原因分析:
- synchronized如果没有指定类对象作为锁对象,默认使用的是实例对象作为锁对象
- 多例模式下,每个线程执行方法用的实例对象都不一样
解决方法:
手动指定synchronized的锁对象为类对象
-
修改service代码
package com.ajie.stock.service.impl; import com.ajie.stock.mapper.StockMapper; import com.ajie.stock.service.StockService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Scope; import org.springframework.context.annotation.ScopedProxyMode; import org.springframework.stereotype.Service; /** * @author ajie * @description: TODO */ @Service @Scope(value = "prototype",proxyMode = ScopedProxyMode.TARGET_CLASS) public class StockServiceImpl implements StockService { @Autowired private StockMapper stockMapper; @Override public String deductStock(Long goodsId, Integer count) { // 指定类对象作为锁对象,解决多例模式锁失效场景 synchronized(StockServiceImpl.class){ // 1.根据商品id查询库存 Integer stock = stockMapper.selectStockByGoodsId(goodsId); // 2.判断库存数量是否足够 if (stock < count) { return "库存不足"; } // 3.库存数量足够,扣减库存 stockMapper.updateStockByGoodsId(goodsId, stock - count); return "库存扣减成功"; } } }
-
查看结果
锁失效现象已经得到解决
事务注解
事务注解场景下,对两种锁都会失效
案例演示:
-
修改service代码
package com.ajie.stock.service.impl; import com.ajie.stock.mapper.StockMapper; import com.ajie.stock.service.StockService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Scope; import org.springframework.context.annotation.ScopedProxyMode; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; /** * @author ajie * @description: TODO */ @Service public class StockServiceImpl implements StockService { @Autowired private StockMapper stockMapper; // 加事务注解 @Transactional @Override public synchronized String deductStock(Long goodsId, Integer count) { // 1.根据商品id查询库存 Integer stock = stockMapper.selectStockByGoodsId(goodsId); // 2.判断库存数量是否足够 if (stock < count) { return "库存不足"; } // 3.库存数量足够,扣减库存 stockMapper.updateStockByGoodsId(goodsId, stock - count); return "库存扣减成功"; } }
-
修改数据中的库存,进行测试
可以看出,使用事务对代码进行控制,锁失效了
事务注解导致锁失效的原理图:
发生这种现象的原因是事务的隔离级别,InnoDB引擎默认支持的是读未提交。
解决方法:
将加锁、解锁动作放在事务外面:
-
service接口代码改造
package com.ajie.stock.service; public interface StockService { String deductStock(Long goodsId, Integer stock); String deduct(Long goodsId, Integer count); }
-
service实现类代码改造
package com.ajie.stock.service.impl; import com.ajie.stock.mapper.StockMapper; import com.ajie.stock.service.StockService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; /** * @author ajie * @description: TODO */ @Service //@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS) public class StockServiceImpl implements StockService { @Autowired private StockMapper stockMapper; @Autowired private StockService owner; @Override public synchronized String deductStock(Long goodsId, Integer count) { return owner.deduct(goodsId, count); } @Transactional public String deduct(Long goodsId, Integer count) { // 1.根据商品id查询库存 Integer stock = stockMapper.selectStockByGoodsId(goodsId); // 2.判断库存数量是否足够 if (stock < count) { return "库存不足"; } // 3.库存数量足够,扣减库存 stockMapper.updateStockByGoodsId(goodsId, stock - count); return "库存扣减成功"; } }
-
解决依赖注入问题
spring: main: # 允许定义相同名称的bean,名称相同,后者覆盖前者 allow-bean-definition-overriding: true # 解决循环依赖问题 allow-circular-references: true
-
修改数据中的库存,进行测试
锁失效现象已经得到解决
集群模式
案例演示:
-
搭建nginx反向代理
配置文件
upstream distribute{ server localhost:9098; server localhost:9099; } server { listen 8000; listen [::]:80; server_name localhost; location /distribute{ proxy_pass http://distribute; } }
-
配置idea启动两个应用实例
-
修改jemeter
-
进行代码测试
失效原因:
无论是synchronized锁还是ReentrantLock锁使用的锁对象都是内存中的数据。当启动多个进程以后,由于不同进程间的内存资源是不共享的,那么同一个进程里面的多个线程还是可以受到锁的控制,但是不同进程间的线程就不受控了
mysql锁
悲观锁
单条update语句
-
mapper层代码改造
package com.ajie.stock.mapper; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import org.apache.ibatis.annotations.Update; /** * @author ajie * @description: TODO */ @Mapper public interface StockMapper { /** * 通过单条update语句实现扣减库存 * @param goodsId * @param count * @return */ @Update("update good_stock set stock = stock-#{count} where goods_id = #{goodsId} and stock>=#{count};") Integer updateStockByGoodsIdAndCount(@Param("goodsId") Long goodsId, @Param("count") Integer count); }
-
service层代码改造
package com.ajie.stock.service.impl; import com.ajie.stock.mapper.StockMapper; import com.ajie.stock.service.StockService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; /** * @author ajie * @description: TODO */ @Service public class StockServiceImpl implements StockService { @Autowired private StockMapper stockMapper; public String deductStock(Long goodsId, Integer count) { Integer update = stockMapper.updateStockByGoodsIdAndCount(goodsId, count); if(update>0){ return "库存扣减成功"; } return "库存不足"; } }
-
进行代码测试
通过上面的结果可以看出,解决了由于并发带来的共享资源问题。同时吞吐量也是比较高的
单条update语句问题:
- 易造成缩范围过大:update语句中,如果更新条件不是索引字段,会升级为表锁
- 无法再程序中获取到扣减库存之前的库存值
- 在复杂业务场景下无法满足业务需求
- 死锁问题
for update
-
mapper层代码改造
package com.ajie.stock.mapper; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import org.apache.ibatis.annotations.Update; /** * @author ajie * @description: TODO */ @Mapper public interface StockMapper { /** * 根据商品id更新库存 * @param goodsId * @param stock * @return */ @Update("update good_stock set stock = #{stock} where goods_id = #{goodsId};") Integer updateStockByGoodsId(@Param("goodsId") Long goodsId, @Param("stock") Integer stock); /** * 根据商品id查询库存 * @param goodsId * @return */ @Select("select stock from good_stock where goods_id = #{goodsId} for update") Integer selectStockByGoodsIdForUpdate(@Param("goodsId") Long goodsId); }
-
service层代码改造
package com.ajie.stock.service.impl; import com.ajie.stock.mapper.StockMapper; import com.ajie.stock.service.StockService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; /** * @author ajie * @description: TODO */ @Service public class StockServiceImpl implements StockService { @Autowired private StockMapper stockMapper; @Transactional @Override public String deductStock(Long goodsId, Integer count) { // 1.根据商品id查询库存 Integer stock = stockMapper.selectStockByGoodsIdForUpdate(goodsId); // 2.判断库存数量是否足够 if (stock < count) { return "库存不足"; } // 3.库存数量足够,扣减库存 stockMapper.updateStockByGoodsId(goodsId, stock - count); return "库存扣减成功"; } }
使用for update会在事务开始之后对查询的数据加锁,知道事务提交以后才释放锁。所以这里一定要对整个方法加事务注解,要让整个方法逻辑执行完以后才能释放锁
-
进行代码测试
通过上面的结果可以看出,实用for update加事务注解也能解决由于并发带来的共享资源问题。但是吞吐量比较低
for update语句问题:
- 易造成缩范围过大(和update语句原理一样)
- 性能较差
- 死锁问题
- select … for update 和普通的select语句读取到的内容不一致:前者可以读取到另一个会话未提交的数据,后者不能读取到
单条update和for update性能对比
## 单条update语句
HTTP请求 6000 55 47 80 114 155 7 824 0.0 812.6777732629013 141.26625355546525 119.0445956928078
## for update方式
HTTP请求 6000 765 778 906 952 1101 152 1447 0.0 64.59878769608423 11.229086142483393 9.462713041418588
乐观锁
乐观锁总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,只在更新的时候会判断一下在此期间别人有没有去更新这个数据
版本号
实现方式:
- 给数据库表增加一列 version
- 读取数据的时候,将version字段一并读出
- 数据每更新一次,version字段加1
- 提交更新时,判断库中的version字段值和之前取出来的version比较
- 相同更新,不相同重试
案例实操:
-
goods_stock表新增version字段
-
新增Stock实体类
package com.ajie.stock.entity; import lombok.Data; /** * @author ajie * @description: TODO */ @Data public class Stock { private Long id; private Long goodsId; private Integer stock; private Integer version; }
-
修改mapper层代码
package com.ajie.stock.mapper; import com.ajie.stock.entity.Stock; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import org.apache.ibatis.annotations.Update; /** * @author ajie * @description: TODO */ @Mapper public interface StockMapper { /** * 根据商品id查询库存和版本号 * @param goodsId * @return */ @Select("select stock,version from good_stock where goods_id = #{goodsId}") Stock selectStockAndVersionByGoodsId(@Param("goodsId") Long goodsId); /** * 根据商品id和版本号更新库存和版本号信息 * @param goodsId * @param stock * @return */ @Update("update good_stock set stock = #{stock},version=#{version}+1 where goods_id = #{goodsId} and version = #{version};") Integer updateStockByGoodsIdAndVersion(@Param("goodsId") Long goodsId, @Param("stock") Integer stock, @Param("version")Integer version); }
-
修改service代码
package com.ajie.stock.service.impl; import com.ajie.stock.entity.Stock; import com.ajie.stock.mapper.StockMapper; import com.ajie.stock.service.StockService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; /** * @author ajie * @description: TODO */ @Service public class StockServiceImpl implements StockService { @Autowired private StockMapper stockMapper; @Override public String deductStock(Long goodsId, Integer count) { Integer result = 0; while (result == 0){ // 1.根据商品id查询库存 Stock stock = stockMapper.selectStockAndVersionByGoodsId(goodsId); // 2.判断库存数量是否足够 if (stock.getStock() < count) { return "库存不足"; } // 3.库存数量足够,扣减库存 result = stockMapper.updateStockByGoodsIdAndVersion(goodsId, stock.getStock() - count, stock.getVersion()); } return "库存扣减成功"; } }
-
进行代码测试
通过上面的结果可以看到,使用乐观锁的方式解决了并发资源共享问题,同时版本号也增加到了6000
时间戳
实现方式:
- 给数据库表增加一列timestamp
- 读取数据的时候,将timestamp字段一并读出
- 数据没更新一次,timestamp取当前时间戳
- 提交更新时,判断库中的timestamp字段和之前取出来的timestamp比较
- 想通更新,不相同重试
实现方式和版本号一样,这里就不做演示了
乐观锁存在的问题:
- 高并发写操作下性能很低:大量线程不断循环,有的循环几千次才能更新成功
- ABA问题。一个线程修改后,又把数据修改回去,另一个线程读取的数据虽然没有变,但是已经是修改过的数据了
分布式锁
分布式锁是控制分布式系统之间同步访问共享资源的额一种方式
普通锁示意图:
分布式锁示意图:
实现方式:
- Redis实现分布式锁
- Zookeeper实现分布式锁
- Mysql实现分布式锁
- Etcd实现分布式锁
实现分布式锁注意的点
- 互斥性
- 可重入性
- 锁超时、防死锁
- 锁释放正确、防误删
- 阻塞和非阻塞
- 公平和非公平
创建抽象类AbstractLock 实现Lock接口,重写里面的方法
package com.ajie.stock.lock;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
/**
* @author ajie
* @description: TODO
*/
public class AbstractLock implements Lock {
@Override
public void lock() {
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public boolean tryLock() {
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
@Override
public void unlock() {
}
@Override
public Condition newCondition() {
return null;
}
}
使用抽象类实现接口的目的方便扩展。将来我们会实现Redis、Zookeeper、mysql、Etcd等各种分布式锁。这些分布式锁有一些共同的属性,还有一些共同的方法,这些方法是Lock接口所不具有的。如果不借助抽象类,那么每种锁的类里面都要写一次,不具备复用性。
Redis实现分布式锁
Redis的特点:
- Redis是高性能的内存数据库,满足高并发的需求
- Redis支持原子性操作,保证操作的原子性和一致性
- Redis支持分布式部署,支持多节点间数据同步和复制,从而提高高可用性和容错性
简化版
-
添加Redis依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
-
yml新增配置
spring: data: redis: host: xxxxxxx # 端口,默认为6379 port: 6379 # 数据库索引 database: 1 # 密码 password: xxxxx # 连接超时时间 timeout: 60s lettuce: pool: # 连接池中的最小空闲连接 min-idle: 0 # 连接池中的最大空闲连接 max-idle: 8 # 连接池的最大数据库连接数 max-active: 8 # #连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: -1ms
-
创建RedisLock集成AbstractLock类
package com.ajie.stock.lock; import org.springframework.data.redis.core.StringRedisTemplate; import java.util.concurrent.TimeUnit; /** * @author ajie * @description: TODO */ public class RedisLock extends AbstractLock { private StringRedisTemplate redisTemplate; public RedisLock(StringRedisTemplate redisTemplate, String lockName) { this.redisTemplate = redisTemplate; this.lockName = lockName; } @Override public void lock() { // 使用setnx指令进行加锁 while (true) { Boolean result = redisTemplate.opsForValue().setIfAbsent(lockName, "1"); if (result != null && result) { break; } // 每次获取锁,如果没拿到,睡眠50毫秒 try { TimeUnit.MILLISECONDS.sleep(50); } catch (InterruptedException e) { throw new RuntimeException(e); } } } @Override public void unlock() { redisTemplate.delete(lockName); } }
-
改造service层方法
package com.ajie.stock.service.impl; import com.ajie.stock.lock.AbstractLock; import com.ajie.stock.lock.RedisLock; import com.ajie.stock.mapper.StockMapper; import com.ajie.stock.service.StockService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; /** * @author ajie * @description: TODO */ @Service public class StockServiceImpl implements StockService { @Autowired private StockMapper stockMapper; @Autowired private StringRedisTemplate redisTemplate; @Override public String deductStock(Long goodsId, Integer count) { AbstractLock lock = null; try { lock = new RedisLock(redisTemplate, "lock" + goodsId); lock.lock(); // 根据商品id查询库存 String stock = redisTemplate.opsForValue().get("stock" + goodsId); // 判断商品是否存在 if (StringUtils.isEmpty(stock)) { return "商品不存在"; } int lastLock = Integer.parseInt(stock); // 2.判断库存数量是否足够 if (lastLock < count) { return "库存不足"; } redisTemplate.opsForValue().set("stock" + goodsId, String.valueOf(lastLock - count)); return "库存扣减成功"; } finally { if (lock != null) { lock.unlock(); } } } }
-
在Redis客户端设置库存为7000
> set stock1 7000 OK > get stock1 7000
-
启动项目进行测试
通过上面的结果可以看到Redis实现的分布式锁成功了。
简易版本的分布式锁解决的问题:
- 互斥性
未解决的问题:
- 可重入性
- 锁超时、防死锁
- 锁释放正确、防误删
- 阻塞和非阻塞
- 公平和非公平
增加过期时间
通过增加过期时间,解决锁超时、死锁问题
-
在抽象类AbstractLock中增加带有过期时间参数的lock方法
package com.ajie.stock.lock; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; /** * @author ajie * @description: TODO */ public abstract class AbstractLock implements Lock { protected String lockName; @Override public Condition newCondition() { return null; } @Override public void unlock() { } @Override public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { return false; } @Override public boolean tryLock() { return false; } @Override public void lockInterruptibly() throws InterruptedException { } @Override public void lock() { } // 新增带有过期时间的lock方法 public abstract void lock(Long expire, TimeUnit unit); }
-
修改RedisLock类的代码
package com.ajie.stock.lock; import org.springframework.data.redis.core.StringRedisTemplate; import java.util.concurrent.TimeUnit; /** * @author ajie * @description: TODO */ public class RedisLock extends AbstractLock { private StringRedisTemplate redisTemplate; // 设置默认的锁过期时间 public static final long DEFAULT_EXPIRE = 30000; public RedisLock(StringRedisTemplate redisTemplate, String lockName) { this.redisTemplate = redisTemplate; this.lockName = lockName; } @Override public void lock() { lock(DEFAULT_EXPIRE,TimeUnit.MILLISECONDS); } @Override public void lock(Long expire, TimeUnit unit) { // 使用setnx指令进行加锁 while (true) { // 使用带有过期时间的设置值的方法 Boolean result = redisTemplate.opsForValue().setIfAbsent(lockName, "1", expire, unit); if (result != null && result) { break; } try { TimeUnit.MILLISECONDS.sleep(50); } catch (InterruptedException e) { throw new RuntimeException(e); } } } @Override public void unlock() { redisTemplate.delete(lockName); } }
-
通过加过期时间,当应用宕机以后,锁在指定时间以后依然可以释放掉
增加UUID
通过增加UUID,解决锁误删问题
误删场景:
- 线程A获取到锁,但是由于业务逻辑在负载增加的时候,执行起来是比较耗时的,在业务逻辑还没执行完,锁过期时间到了。线程A获得的锁没有了
- 此时线程B就可以获取到锁,在线程B执行的过程中,线程A的逻辑执行完毕,开始删除锁
- 线程A的锁已经没了,此时删除的锁是B所持有的锁,这就是锁的误删
改造代码,增加UUID,修改RedisLock
package com.ajie.stock.lock;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.concurrent.TimeUnit;
/**
* @author ajie
* @description: TODO
*/
public class RedisLock extends AbstractLock {
private StringRedisTemplate redisTemplate;
public static final long DEFAULT_EXPIRE = 30000;
public final String UUID;
public RedisLock(StringRedisTemplate redisTemplate, String lockName) {
this.redisTemplate = redisTemplate;
this.lockName = lockName;
// 增加UUID,解决锁误删问题
this.UUID = java.util.UUID.randomUUID().toString().replaceAll("-", "");
}
@Override
public void lock() {
lock(DEFAULT_EXPIRE, TimeUnit.MILLISECONDS);
}
@Override
public void lock(Long expire, TimeUnit unit) {
// 使用setnx指令进行加锁
while (true) {
Boolean result = redisTemplate.opsForValue().setIfAbsent(lockName, UUID,
expire, unit);
if (result != null && result) {
break;
}
try {
TimeUnit.MILLISECONDS.sleep(50);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
@Override
public void unlock() {
String uuid = redisTemplate.opsForValue().get(lockName);
// 先判断是否是当前线程的锁
if (uuid != null && uuid.equals(UUID)) {
redisTemplate.delete(lockName);
}
}
}
初识Lua脚本
上面实现的分布式锁存在三个问题:
- 加锁逻辑使用了Redis的get、set指令,不是原子性的,中间会有穿插
- 释放锁的逻辑也是的,现获取值,再释放锁,这两步操作也不是原子性,也会出现指令的并发问题
- 对于公平和非公平锁的实现,需要复杂的Redis指令实现,这个也需要保证指令执行的原子性
基于上面的问题可以使用Lua脚本去实现。Redis支持Lua脚本,对于Lua脚本代码的执行是原子性操作的
Lua介绍
Lua是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放,其设计的目的是为了嵌入到程序中,从而为应用程序提供灵活的扩展和定制功能
Lua特性
- 轻量级:它用标准C语言编写并以源代码形式开放,编译后仅仅100余K,可以很方便的嵌入到别的应用程序里
- 可扩展:Lua提供了非常易于使用的扩展接口和定制:由宿主语言(通常是C或C++)提供这些功能,Lua可以使用他们,就像是本地内置的功能一样
Lua安装
前往github下载Windows版本的可执行文件
https://github.com/rjpcomputing/luaforwindows/releases/tag/v5.1.5-52
点击可执行文件,一直下一步就可以了
测试安装
-
在项目根目录创建test.lua文件
-
编写测试代码
print('hello world')
-
在cmd窗口执行
数据类型
数据类型 | 描述 |
---|---|
nil | 这个最简单,只有nil属于该类,表示一个无效值(在条件表达式中相当于false) |
boolean | 包含两个值:false和true |
number | 表示双精度类型的实浮点数 |
string | 字符串由一对双引号或单引号来表示 |
function | 由C或lua编写的函数 |
table | Lua中的表(table)其实是一个"关联数组",数组的索引可以是数字,字符串或表类型。在Lua中,table的创建是通过“构造表达式”来完成的,最简单构造表达式是{},用来创建一个空表 |
-- 数据类型
print(type("hello world")) -- string
print(type(1)) -- number
print(type(2.2)) -- number
print(type(print)) -- function
print(type(type)) -- function
print(type(nil)) -- nil
print(type(true)) -- boolean
print(type({"1","2"})) -- table
变量
Lua变量有三种类型:全局变量、局部变量、表中的域
- Lua中的变量全是全局变量,哪怕是语句块或是函数里,除非用local显示声明为局部变量
- 局部变量的作用域为从声明位置开始到所在语句块结束
- 变量的默认值均为nil
-- 变量
a = 5 -- 全局变量
print(a) -- 5
print(c) -- nil
local b = 4
print(b) -- 4
do
local x = 1
c = 7
print(x) -- 1
print(c) -- 7
end
print(x) -- nil
print(c) -- 7
-- 多变量赋值
a, b, c = 2, 3
print(a .. b) -- 23
print(c) -- nil
a, b = 2, 3, 4
print(a .. b) -- 23
-- table赋值
tal = {key='a',key2='b'}
print(tal['key']) -- a
tal['key'] = 'c'
print(tal['key'] -- c
print(tal['key2']) -- b
循环
-- while循环
a = 1
while (a < 5) do
print(a)
a = a + 1
end
-- for循环
--[[
从exp1开始循环到exp2,exp3是步长
for var=exp1,epx2,exp3 do
end
]]
for i = 1, 5, 1 do
print(i)
end
for i = 5, 1, -1 do
print(i)
end
流程控制
-- 流程控制
a=9
if a<10 then
print('小于10')
end
a=10
if a<10 then
print('小于10')
else
print('大于等于10')
end
a=10
if a<10 then
print('小于10')
elseif a==10 then
print('等于10')
else
print('大于10')
end
函数
-- 函数
function xx(a)
print(a)
end
xx(10)
xxx = function(result)
print(result)
end
function sum(a,b,func1)
sum = a+b
func1(sum)
end
sum(10,20,xxx)
算术运算符
运算符 | 描述 | 示例 |
---|---|---|
+ | 加法 | 10+20 输出结果 30 |
- | 减法 | 20-10 输出结果 10 |
* | 乘法 | 10*20 输出结果 200 |
/ | 除法 | 20/10 输出结果 2 |
% | 取余 | 20%10 输出结果 0 |
^ | 乘幂 | 10^2 输出结果 100 |
- | 负号 | -10 输出结果 -10 |
// | 整除运算符(>=lua5.3) | 5//2 输出结果 2 |
-- 算术运算符
print(1 + 2)
print(2 - 1)
print(2 * 1)
print(-10)
print(10 / 6)
print(10 % 6)
print(10 // 6)
print(10 ^ 2)
关系运算符
运算符 | 描述 | 示例 |
---|---|---|
== | 等于,检查左右两边的值是否相等,相等为true,不相等为false | 10==20为false |
~= | 不等于,检查左右两边的值是否不相等,不相等为true,相等为false | 10~=20为true |
> | 大于,如果左边的值大于右边的值,返回true,否则返回false | 10>20为false |
< | 小于,如果左边的值小于右边的值,返回true,否则返回false | 10<20为true |
>= | 大于等于,如果左边的值大于等于右边的值,返回true,否则返回false | |
<= | 小于等于,如果左边的值小于等于右边的值,返回true,否则返回false |
逻辑运算符
运算符 | 描述 | 示例 |
---|---|---|
and | 逻辑与操作符。若A为false,则返回A,否则返回B | (A and B) |
or | 逻辑或操作符。若A为true,则返回A,否则返回B | (A or B) |
not | 逻辑非操作符。与逻辑运算结果相反,如果条件结果为true,逻辑非为false | not (A and B) |
其它运算符
运算符 | 描述 | 示例 |
---|---|---|
… | 连接两个字符串 | a…b。其中a为"hello “,b为"world”。输出结果为"hello world" |
# | 一元运算符,返回字符串长度或表的长度 | #"hello"的结果为5 |
Lua脚本在Redis中使用
-
eval执行Lua脚本
EVAL script numkeys key [key...] arg [arg...] > eval "return redis.call('set',KEYS[1],ARGV[1])" 1 name xiaoming OK > keys * name stock1 > get name xiaoming > eval "return redis.call('set',KEYS[1],ARGV[1])" 1 name xiaohong OK > get name xiaohong
-
将脚本加载到Redis中
当我们把Lua脚本加载到Redis中,这个脚本并不会立马执行,而是会缓存起来,并且会返回sha1校验和,后期我们可以通过EVALSHA来执行这个脚本
> script load "return redis.call('set',KEYS[1],ARGV[1])" c686f316aaf1eb01d5a4de1b0b63cd233010e63d
-
通过evalsha执行lua脚本
> evalsha c686f316aaf1eb01d5a4de1b0b63cd233010e63d 1 name xiaozhang OK > get name xiaozhang
-
判断脚本是否存在Redis中
> script exists c686f316aaf1eb01d5a4de1b0b63cd233010e63d 1
-
将脚本从Redis中移除
> script flush OK > script exists c686f316aaf1eb01d5a4de1b0b63cd233010e63d 0
Lua脚本实现分布式锁
Redis客户端演示
-
加锁的lua脚本
-- 判断一下当前是否存在这把锁,如果锁存在,直接加锁失败,如果锁不存在,那么就set一把锁,并且给锁一个过期时间 if (redis.call('exists', lockName) == 0) then redis.call('set', lockName, uuid) redis.call('pexpire', lockName, 30000) return 1 else return 0 end
放到Redis客户端执行
# 加锁成功 > eval "if (redis.call('exists', KEYS[1]) == 0) then redis.call('set', KEYS[1], ARGV[1]) redis.call('pexpire', KEYS[1], ARGV[2]) return 1 else return 0 end" 1 lockName uuid 30000 1 # 查看过期时间 > ttl lockName 15 > ttl lockName 13 # 再次加锁失败 > eval "if (redis.call('exists', KEYS[1]) == 0) then redis.call('set', KEYS[1], ARGV[1]) redis.call('pexpire', KEYS[1], ARGV[2]) return 1 else return 0 end" 1 lockName uuid 30000 0 > ttl lockName 8
-
解锁的lua脚本
-- 简易锁的释放锁lua -- 判断锁是否存在,如果不存在直接return,如果存在,那么我们需要判断UUID是否是当前线程的UUID,如果等于,那么就del,如果不等于就return if(redis.call('exists',lockName) == 0) then return 0 elseif (redis.call('get', lockName) == uuid) then redis.call('del',lockName) return 1 else return 0 end
放到Redis客户端执行
# 没有锁,执行失败 > eval "if(redis.call('exists',KEYS[1]) == 0) then return 0 elseif (redis.call('get', KEYS[1]) == ARGV[1]) then redis.call('del',KEYS[1]) return 1 else return 0 end" 1 lockName uuid 0 # 加锁脚本 > eval "if (redis.call('exists', KEYS[1]) == 0) then redis.call('set', KEYS[1], ARGV[1]) redis.call('pexpire', KEYS[1], ARGV[2]) return 1 else return 0 end" 1 lockName uuid 30000 1 # 查看锁的过期时间 > ttl lockName 27 # 执行释放锁的脚本 > eval "if(redis.call('exists',KEYS[1]) == 0) then return 0 elseif (redis.call('get', KEYS[1]) == ARGV[1]) then redis.call('del',KEYS[1]) return 1 else return 0 end" 1 lockName uuid 1 > ttl lockName -2
java代码改造
package com.ajie.stock.lock;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
/**
* @author ajie
* @description: TODO
*/
public class RedisLock extends AbstractLock {
private StringRedisTemplate redisTemplate;
public static final Long DEFAULT_EXPIRE = 30000L;
public final String UUID;
public RedisLock(StringRedisTemplate redisTemplate, String lockName) {
this.redisTemplate = redisTemplate;
this.lockName = lockName;
this.UUID = java.util.UUID.randomUUID().toString().replaceAll("-", "");
}
@Override
public void lock() {
lock(DEFAULT_EXPIRE, TimeUnit.MILLISECONDS);
}
@Override
public void lock(Long expire, TimeUnit unit) {
// 加锁的lua脚本
String lockScript = """
if (redis.call('exists', KEYS[1]) == 0) then redis.call('set', KEYS[1], ARGV[1])
redis.call('pexpire', KEYS[1], ARGV[2]) return 1 else return 0 end
""";
// 使用setnx指令进行加锁
while (true) {
Long result = redisTemplate.execute(new DefaultRedisScript<>(lockScript, Long.class),
Collections.singletonList(lockName),
UUID, DEFAULT_EXPIRE.toString());
if (result != null && result == 1) {
break;
}
try {
TimeUnit.MILLISECONDS.sleep(50);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
@Override
public void unlock() {
// 释放锁的lua脚本
String unLockScript = """
if(redis.call('exists',KEYS[1]) == 0) then return 0 elseif (redis.call('get',
KEYS[1]) == ARGV[1]) then redis.call('del',KEYS[1]) return 1 else return 0 end
""";
redisTemplate.execute(new DefaultRedisScript<>(unLockScript, Long.class),
Collections.singletonList(lockName), UUID);
}
}
Lua+hash实现可重入锁
实现原理:
- 使用Redis的hash结构。key为锁的名称,field为uuid,value为锁的加锁次数
- 没重入一次,加锁次数加一
- 当释放锁的时候对加锁次数减一
- 当加锁次数减到不大于0以后,就删除这个锁的key
Redis客户端演示
-
加锁的lua脚本
-- 可重入锁的加锁lua脚本 -- 判断锁是否存在,如果不存在,直接加锁,给重入次数设置为1,设置过期时间 -- 如果存在,对原有的锁重入次数加1,重新设置过期时间 if (redis.call('exists', lockName) == 0) then redis.call('hincrby', lockName, uuid, 1) redis.call('pexpire', lockName, 30000) return 1 end if (redis.call('hexists', lockName, uuid) == 1) then redis.call('hincrby', lockName, uuid, 1) redis.call('pexpire', lockName, 30000) return 1 else return 0 end
redis命令行执行
> eval "if (redis.call('exists', KEYS[1]) == 0) then redis.call('hincrby', KEYS[1], ARGV[1], 1) redis.call('pexpire', KEYS[1], ARGV[2]) return 1 end if (redis.call('hexists', KEYS[1], ARGV[1]) == 1) then redis.call('hincrby', KEYS[1], ARGV[1], 1) redis.call('pexpire', KEYS[1], ARGV[2]) return 1 else return 0 end" 1 lockName uuid 30000 1 > hget lockName uuid 1 > eval "if (redis.call('exists', KEYS[1]) == 0) then redis.call('hincrby', KEYS[1], ARGV[1], 1) redis.call('pexpire', KEYS[1], ARGV[2]) return 1 end if (redis.call('hexists', KEYS[1], ARGV[1]) == 1) then redis.call('hincrby', KEYS[1], ARGV[1], 1) redis.call('pexpire', KEYS[1], ARGV[2]) return 1 else return 0 end" 1 lockName uuid 30000 1 > eval "if (redis.call('exists', KEYS[1]) == 0) then redis.call('hincrby', KEYS[1], ARGV[1], 1) redis.call('pexpire', KEYS[1], ARGV[2]) return 1 end if (redis.call('hexists', KEYS[1], ARGV[1]) == 1) then redis.call('hincrby', KEYS[1], ARGV[1], 1) redis.call('pexpire', KEYS[1], ARGV[2]) return 1 else return 0 end" 1 lockName uuid 30000 1 > hget lockName uuid 3
-
释放锁的脚本
-- 可重入锁的释放锁lua脚本 -- 判断当前持有锁的线程是否是本线程,如果不是就不需要释放了 -- 如果是,就对可重入次数-1,-1之后,判断是否大于0,如果大于0,重新设置过期时间,否则删除锁 if (redis.call('hexists', lockName, uuid) == 0) then return 0 end local lockCount = redis.call('hincrby', lockName, uuid, -1) if (lockCount > 0) then redis.call('pexpire', lockName, 30000) else redis.call('del', lockName) end return 1
redis命令行执行
> hget lockName uuid 3 > eval "if(redis.call('hexists',KEYS[1],ARGV[1])==0) then return 0; end local lockCount = redis.call('hincrby',KEYS[1],ARGV[1],-1) if(lockCount>0) then redis.call('pexpire',KEYS[1],ARGV[2]) else redis.call('del',KEYS[1]) end return 1" 1 lockName uuid 30000 1 > hget lockName uuid 2 > eval "if(redis.call('hexists',KEYS[1],ARGV[1])==0) then return 0; end local lockCount = redis.call('hincrby',KEYS[1],ARGV[1],-1) if(lockCount>0) then redis.call('pexpire',KEYS[1],ARGV[2]) else redis.call('del',KEYS[1]) end return 1" 1 lockName uuid 30000 1 > eval "if(redis.call('hexists',KEYS[1],ARGV[1])==0) then return 0; end local lockCount = redis.call('hincrby',KEYS[1],ARGV[1],-1) if(lockCount>0) then redis.call('pexpire',KEYS[1],ARGV[2]) else redis.call('del',KEYS[1]) end return 1" 1 lockName uuid 30000 1 > hget lockName uuid null
java代码改造
RedisLock
package com.ajie.stock.lock;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
/**
* @author ajie
* @description: TODO
*/
public class RedisLock extends AbstractLock {
private StringRedisTemplate redisTemplate;
public static final Long DEFAULT_EXPIRE = 30000L;
public final String UUID;
public RedisLock(StringRedisTemplate redisTemplate, String lockName) {
this.redisTemplate = redisTemplate;
this.lockName = lockName;
this.UUID = java.util.UUID.randomUUID().toString().replaceAll("-", "");
}
@Override
public void lock() {
lock(DEFAULT_EXPIRE, TimeUnit.MILLISECONDS);
}
@Override
public void lock(Long expire, TimeUnit unit) {
// 加锁的lua脚本
String lockScript = """
if (redis.call('exists', KEYS[1]) == 0) then redis.call('hincrby', KEYS[1],
ARGV[1], 1) redis.call('pexpire', KEYS[1], ARGV[2]) return 1 end if
(redis.call('hexists', KEYS[1], ARGV[1]) == 1) then redis.call('hincrby', KEYS[1],
ARGV[1], 1) redis.call('pexpire', KEYS[1], ARGV[2]) return 1 else return 0 end
""";
// 使用setnx指令进行加锁
while (true) {
Long result = redisTemplate.execute(new DefaultRedisScript<>(lockScript, Long.class),
Collections.singletonList(lockName),
UUID, DEFAULT_EXPIRE.toString());
/*Boolean result = redisTemplate.opsForValue().setIfAbsent(lockName, UUID,
expire, unit);
if (result != null && result) {
break;
}*/
if (result != null && result == 1) {
break;
}
try {
TimeUnit.MILLISECONDS.sleep(50);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
@Override
public void unlock() {
// 释放锁的lua脚本
String unLockScript = """
if(redis.call('hexists',KEYS[1],ARGV[1])==0) then return 0; end local lockCount =
redis.call('hincrby',KEYS[1],ARGV[1],-1) if(lockCount>0) then redis.call('pexpire',
KEYS[1],ARGV[2]) else redis.call('del',KEYS[1]) end return 1
""";
redisTemplate.execute(new DefaultRedisScript<>(unLockScript, Long.class),
Collections.singletonList(lockName), UUID);
/*String uuid = redisTemplate.opsForValue().get(lockName);
if (uuid != null && uuid.equals(UUID)) {
redisTemplate.delete(lockName);
}*/
}
}
service实现类代码
package com.ajie.stock.service.impl;
import com.ajie.stock.lock.AbstractLock;
import com.ajie.stock.lock.RedisLock;
import com.ajie.stock.mapper.StockMapper;
import com.ajie.stock.service.StockService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
/**
* @author ajie
* @description: TODO
*/
@Service
public class StockServiceImpl implements StockService {
@Autowired
private StockMapper stockMapper;
@Autowired
private StringRedisTemplate redisTemplate;
@Override
public String deductStock(Long goodsId, Integer count) {
AbstractLock lock = null;
try {
lock = new RedisLock(redisTemplate, "lock" + goodsId);
lock.lock();
// 实现锁的重入效果
lock(lock);
// 根据商品id查询库存
String stock = redisTemplate.opsForValue().get("stock" + goodsId);
// 判断商品是否存在
if (StringUtils.isEmpty(stock)) {
return "商品不存在";
}
int lastLock = Integer.parseInt(stock);
// 2.判断库存数量是否足够
if (lastLock < count) {
return "库存不足";
}
redisTemplate.opsForValue().set("stock" + goodsId, String.valueOf(lastLock - count));
return "库存扣减成功";
} finally {
if (lock != null) {
lock.unlock();
}
}
}
private void lock(AbstractLock lock) {
lock.lock();
lock.unlock();
}
}
redis锁续期
为什么需要锁续期?
由于前面对锁加了过期时间,有时候,一个线程执行业务逻辑确实需要花费比过期时间还要长的时间。如果在线程执行过程中就释放了锁。另一个线程就会获取到锁,同样会出现并发问题。由于正常业务执行导致的锁过期需要对锁的过期时间延长
实现方式:在同一个进程中开启异步线程,对锁进行时间重置的工作。当此进程宕机以后,也就不会再进行锁续期,锁也能正常释放掉了
lua脚本实现锁需求:
-- lua脚本实现锁自动续期
-- 判断当前持有锁的线程是否是本线程,如果是就进行续期,否则直接返回
if(redis.call('hexists',lockName,uuid) == 0) then
return 0;
else
redis.call('pexpire',lockName,30000)
end
redis客户端执行
# 没有锁,续期失败
> eval "if(redis.call('hexists',KEYS[1],ARGV[1]) == 0) then return 0; else redis.call('pexpire',KEYS[1],ARGV[2]) end" 1 lockName uuid 30000
0
# 加锁
> eval "if (redis.call('exists', KEYS[1]) == 0) then redis.call('hincrby', KEYS[1], ARGV[1], 1) redis.call('pexpire', KEYS[1], ARGV[2]) return 1 end if (redis.call('hexists', KEYS[1], ARGV[1]) == 1) then redis.call('hincrby', KEYS[1], ARGV[1], 1) redis.call('pexpire', KEYS[1], ARGV[2]) return 1 else return 0 end" 1 lockName uuid 30000
1
> ttl lockName
25
# 进行锁续期
> eval "if (redis.call('exists', KEYS[1]) == 0) then redis.call('hincrby', KEYS[1], ARGV[1], 1) redis.call('pexpire', KEYS[1], ARGV[2]) return 1 end if (redis.call('hexists', KEYS[1], ARGV[1]) == 1) then redis.call('hincrby', KEYS[1], ARGV[1], 1) redis.call('pexpire', KEYS[1], ARGV[2]) return 1 else return 0 end" 1 lockName uuid 30000
1
> ttl lockName
28
java代码改造
package com.ajie.stock.lock;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
/**
* @author ajie
* @description: TODO
*/
public class RedisLock extends AbstractLock {
private StringRedisTemplate redisTemplate;
public static final Long DEFAULT_EXPIRE = 30000L;
public final String UUID;
public RedisLock(StringRedisTemplate redisTemplate, String lockName) {
this.redisTemplate = redisTemplate;
this.lockName = lockName;
this.UUID = java.util.UUID.randomUUID().toString().replaceAll("-", "");
}
@Override
public void lock() {
lock(DEFAULT_EXPIRE, TimeUnit.MILLISECONDS);
}
@Override
public void lock(Long expire, TimeUnit unit) {
// 加锁的lua脚本
String lockScript = """
if (redis.call('exists', KEYS[1]) == 0) then redis.call('hincrby', KEYS[1],
ARGV[1], 1) redis.call('pexpire', KEYS[1], ARGV[2]) return 1 end if
(redis.call('hexists', KEYS[1], ARGV[1]) == 1) then redis.call('hincrby', KEYS[1],
ARGV[1], 1) redis.call('pexpire', KEYS[1], ARGV[2]) return 1 else return 0 end
""";
// 使用setnx指令进行加锁
while (true) {
Long result = redisTemplate.execute(new DefaultRedisScript<>(lockScript, Long.class),
Collections.singletonList(lockName),
UUID, DEFAULT_EXPIRE.toString());
/*Boolean result = redisTemplate.opsForValue().setIfAbsent(lockName, UUID,
expire, unit);
if (result != null && result) {
break;
}*/
if (result != null && result == 1) {
// 开启异步线程进行锁续期工作
new Thread(()->{
while(true) {
String expireLua = """
if(redis.call('hexists',KEYS[1],ARGV[1]) == 0) then
return 0;
else
redis.call('pexpire',KEYS[1],ARGV[2])
end
""";
Long expireResult = redisTemplate.execute(new DefaultRedisScript<>(expireLua, Long.class),
Collections.singletonList(lockName),
UUID, DEFAULT_EXPIRE.toString());
if(expireResult != null && expireResult == 0){
break;
}
try {
// 为了减少和Redis的交互,当过期时间过了一半再续期
TimeUnit.MILLISECONDS.sleep(DEFAULT_EXPIRE/2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}).start();
break;
}
try {
TimeUnit.MILLISECONDS.sleep(50);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
@Override
public void unlock() {
// 释放锁的lua脚本
String unLockScript = """
if(redis.call('hexists',KEYS[1],ARGV[1])==0) then return 0; end local lockCount =
redis.call('hincrby',KEYS[1],ARGV[1],-1) if(lockCount>0) then redis.call('pexpire',
KEYS[1],ARGV[2]) else redis.call('del',KEYS[1]) end return 1
""";
redisTemplate.execute(new DefaultRedisScript<>(unLockScript, Long.class),
Collections.singletonList(lockName),
UUID, DEFAULT_EXPIRE.toString());
/*String uuid = redisTemplate.opsForValue().get(lockName);
if (uuid != null && uuid.equals(UUID)) {
redisTemplate.delete(lockName);
}*/
}
}
增加锁获取时间
通过增加锁获取时间,解决锁一直阻塞等待的问题。通过指定锁的获取时间,在指定时间内,线程可以一直循环获取锁,当超过这个时间,线程就跳出循环,不再获取锁了
java代码改造
AbstractLock
package com.ajie.stock.lock;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
/**
* @author ajie
* @description: TODO
*/
public abstract class AbstractLock implements Lock {
protected String lockName;
@Override
public Condition newCondition() {
return null;
}
@Override
public void unlock() {
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
// 增加重载方法,支持设置过期时间
protected boolean tryLock(long time,Long expire, TimeUnit unit) throws InterruptedException {
return false;
}
@Override
public boolean tryLock() {
return false;
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public void lock() {
}
public abstract void lock(Long expire, TimeUnit unit);
}
RedisLock
package com.ajie.stock.lock;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
/**
* @author ajie
* @description: TODO
*/
public class RedisLock extends AbstractLock {
private StringRedisTemplate redisTemplate;
public static final Long DEFAULT_EXPIRE = 30000L;
public final String UUID;
public RedisLock(StringRedisTemplate redisTemplate, String lockName) {
this.redisTemplate = redisTemplate;
this.lockName = lockName;
this.UUID = java.util.UUID.randomUUID().toString().replaceAll("-", "");
}
@Override
public void lock() {
lock(DEFAULT_EXPIRE, TimeUnit.MILLISECONDS);
}
@Override
public void lock(Long expire, TimeUnit unit) {
// 加锁的lua脚本
String lockScript = """
if (redis.call('exists', KEYS[1]) == 0) then redis.call('hincrby', KEYS[1],
ARGV[1], 1) redis.call('pexpire', KEYS[1], ARGV[2]) return 1 end if
(redis.call('hexists', KEYS[1], ARGV[1]) == 1) then redis.call('hincrby', KEYS[1],
ARGV[1], 1) redis.call('pexpire', KEYS[1], ARGV[2]) return 1 else return 0 end
""";
// 使用setnx指令进行加锁
while (true) {
Long result = redisTemplate.execute(new DefaultRedisScript<>(lockScript, Long.class),
Collections.singletonList(lockName),
UUID, expire.toString());
/*Boolean result = redisTemplate.opsForValue().setIfAbsent(lockName, UUID,
expire, unit);
if (result != null && result) {
break;
}*/
if (result != null && result == 1) {
new Thread(() -> {
while (true) {
String expireLua = """
if(redis.call('hexists',KEYS[1],ARGV[1]) == 0) then
return 0;
else
redis.call('pexpire',KEYS[1],ARGV[2])
end
""";
Long expireResult = redisTemplate.execute(new DefaultRedisScript<>(expireLua, Long.class),
Collections.singletonList(lockName),
UUID, expire.toString());
if (expireResult != null && expireResult == 0) {
break;
}
try {
// 为了减少和Redis的交互,当过期时间过了一半再续期
TimeUnit.MILLISECONDS.sleep(expire / 2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}).start();
break;
}
try {
TimeUnit.MILLISECONDS.sleep(50);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
private boolean tryLockInternal(Long expire, TimeUnit unit) {
// 加锁的lua脚本
String lockScript = """
if (redis.call('exists', KEYS[1]) == 0) then redis.call('hincrby', KEYS[1],
ARGV[1], 1) redis.call('pexpire', KEYS[1], ARGV[2]) return 1 end if
(redis.call('hexists', KEYS[1], ARGV[1]) == 1) then redis.call('hincrby', KEYS[1],
ARGV[1], 1) redis.call('pexpire', KEYS[1], ARGV[2]) return 1 else return 0 end
""";
// 使用setnx指令进行加锁
Long result = redisTemplate.execute(new DefaultRedisScript<>(lockScript, Long.class),
Collections.singletonList(lockName),
UUID, expire.toString());
if (result != null && result == 1) {
new Thread(() -> {
while (true) {
String expireLua = """
if(redis.call('hexists',KEYS[1],ARGV[1]) == 0) then
return 0;
else
redis.call('pexpire',KEYS[1],ARGV[2])
end
""";
Long expireResult = redisTemplate.execute(new DefaultRedisScript<>(expireLua, Long.class),
Collections.singletonList(lockName),
UUID, expire.toString());
if (expireResult != null && expireResult == 0) {
break;
}
try {
// 为了减少和Redis的交互,当过期时间过了一半再续期
TimeUnit.MILLISECONDS.sleep(DEFAULT_EXPIRE / 2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}).start();
return true;
}
try {
TimeUnit.MILLISECONDS.sleep(50);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return tryLock(time, DEFAULT_EXPIRE, unit);
}
// 尝试在指定时间内获取锁
@Override
protected boolean tryLock(long time, Long expire, TimeUnit unit) throws InterruptedException {
// 先记录一个开始时间,然后再记录当前时间,当当前时间-开始时间>time,跳出循环,否则就一直等待获取锁
long startTime = System.currentTimeMillis();
long currentTime = System.currentTimeMillis();
boolean lockResult = false;
while (currentTime - startTime <= time) {
// 获取锁
boolean result = tryLockInternal(expire, unit);
if (result) {
lockResult = true;
break;
}
currentTime = System.currentTimeMillis();
}
return lockResult;
}
@Override
public void unlock() {
// 释放锁的lua脚本
String unLockScript = """
if(redis.call('hexists',KEYS[1],ARGV[1])==0) then return 0; end local lockCount =
redis.call('hincrby',KEYS[1],ARGV[1],-1) if(lockCount>0) then redis.call('pexpire',
KEYS[1],ARGV[2]) else redis.call('del',KEYS[1]) end return 1
""";
redisTemplate.execute(new DefaultRedisScript<>(unLockScript, Long.class),
Collections.singletonList(lockName),
UUID, DEFAULT_EXPIRE.toString());
}
}
service代码
package com.ajie.stock.service.impl;
import com.ajie.stock.lock.AbstractLock;
import com.ajie.stock.lock.RedisLock;
import com.ajie.stock.mapper.StockMapper;
import com.ajie.stock.service.StockService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.concurrent.TimeUnit;
/**
* @author ajie
* @description: TODO
*/
@Service
public class StockServiceImpl implements StockService {
@Autowired
private StockMapper stockMapper;
@Autowired
private StringRedisTemplate redisTemplate;
@Override
public String deductStock(Long goodsId, Integer count) {
AbstractLock lock = null;
try {
lock = new RedisLock(redisTemplate, "lock" + goodsId);
// 尝试在指定时间内获取锁
boolean result = lock.tryLock(5000, TimeUnit.MILLISECONDS);
if (result) {
try {
TimeUnit.MILLISECONDS.sleep(200000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 根据商品id查询库存
String stock = redisTemplate.opsForValue().get("stock" + goodsId);
// 判断商品是否存在
if (StringUtils.isEmpty(stock)) {
return "商品不存在";
}
int lastLock = Integer.parseInt(stock);
// 2.判断库存数量是否足够
if (lastLock < count) {
return "库存不足";
}
redisTemplate.opsForValue().set("stock" + goodsId, String.valueOf(lastLock - count));
return "库存扣减成功";
}
System.out.println("获取锁超时");
return "系统繁忙";
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
if (lock != null) {
lock.unlock();
}
}
}
}
Redisson源码剖析
分布式锁示例
-
导入依赖
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.32.0</version> </dependency>
-
创建配置类
package com.ajie.stock.config; import org.redisson.Redisson; import org.redisson.api.RedissonClient; import org.redisson.config.Config; import org.springframework.boot.SpringBootConfiguration; import org.springframework.context.annotation.Bean; /** * @author ajie * @description: TODO */ @SpringBootConfiguration public class RedissonConfig { @Bean public RedissonClient redissonClient() { Config config = new Config(); config.useSingleServer() .setAddress("redis://127.0.0.1:6379") .setDatabase(1) .setPassword("^Koron#3438$"); return Redisson.create(config); } }
-
修改业务逻辑
package com.ajie.stock.service.impl; import com.ajie.stock.lock.AbstractLock; import com.ajie.stock.mapper.StockMapper; import com.ajie.stock.service.StockService; import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; /** * @author ajie * @description: TODO */ @Service public class StockServiceImpl implements StockService { @Autowired private StockMapper stockMapper; @Autowired private StringRedisTemplate redisTemplate; @Autowired private RedissonClient redissonClient; @Override public String deductStock(Long goodsId, Integer count) { RLock lock = null; try { lock = redissonClient.getLock("lock"+goodsId); lock.lock(); // 根据商品id查询库存 String stock = redisTemplate.opsForValue().get("stock" + goodsId); // 判断商品是否存在 if (StringUtils.isEmpty(stock)) { return "商品不存在"; } int lastLock = Integer.parseInt(stock); // 2.判断库存数量是否足够 if (lastLock < count) { return "库存不足"; } redisTemplate.opsForValue().set("stock" + goodsId, String.valueOf(lastLock - count)); return "库存扣减成功"; } catch (Exception e) { throw new RuntimeException(e); } finally { if (lock != null) { lock.unlock(); } } } }
源码导入
源码下载地址:https://github.com/redisson/redisson
选择3.20.0的版本,高版本需要jdk21
-
下载zip压缩包到指定文件夹,然后解压
-
使用idea打开,配置maven、jdk。导入依赖
-
进行单元测试,校验源码是否导入成功
-
修改BaseTest配置
public static Config createConfig() { // String redisAddress = System.getProperty("redisAddress"); // if (redisAddress == null) { // redisAddress = "127.0.0.1:6379"; // } Config config = new Config(); // config.setCodec(new MsgPackJacksonCodec()); // config.useSentinelServers().setMasterName("mymaster").addSentinelAddress("127.0.0.1:26379", "127.0.0.1:26389"); // config.useClusterServers().addNodeAddress("127.0.0.1:7004", "127.0.0.1:7001", "127.0.0.1:7000"); // 注释系统自带的配置设置 /* config.useSingleServer() .setAddress(RedisRunner.getDefaultRedisServerBindAddressAndPort());*/ // 设置自己的redis地址 config.useSingleServer() .setAddress("redis://localhost:6379") .setDatabase(1) .setPassword("xxxxxx"); // .setPassword("mypass1"); // config.useMasterSlaveConnection() // .setMasterAddress("127.0.0.1:6379") // .addSlaveAddress("127.0.0.1:6399") // .addSlaveAddress("127.0.0.1:6389"); return config; }
-
注释掉测试启动前的代码,否则每次执行都会清楚redis中的key
@BeforeEach public void before() throws IOException, InterruptedException { /*if (flushBetweenTests()) { redisson.getKeys().flushall(); }*/ }
-
修改RedissonRuntimeEnvironment类中的常量地址,根据自己的系统修改相应的地址
private static final String MAC_PATH = "/usr/local/opt/redis/bin/redis-server"; private static final String WINDOW_PATH = "D:\\develop\\redis\\redis-server.exe";
-
执行测试方法testIsLocked
-
可重入锁
-
获取RedissonClient客户端源码:Redisson.create(config)
// --------------1 Redisson public static RedissonClient create(Config config) { return new Redisson(config); } // --------------2 Redisson protected Redisson(Config config) { // 将配置对象存放在Redisson对象中 this.config = config; // 重新拷贝一份 Config configCopy = new Config(config); // 创建连接管理器 connectionManager = ConfigSupport.createConnectionManager(configCopy); RedissonObjectBuilder objectBuilder = null; if (config.isReferenceEnabled()) { objectBuilder = new RedissonObjectBuilder(this); } // 创建命令行客户端,将连接器传进去 commandExecutor = new CommandSyncService(connectionManager, objectBuilder); evictionScheduler = new EvictionScheduler(commandExecutor); writeBehindService = new WriteBehindService(commandExecutor); } // --------------3 ConfigSupport 根据配置对象中指定连接器创建 public static ConnectionManager createConnectionManager(Config configCopy) { ServiceManager serviceManager = new ServiceManager(configCopy); ConnectionManager cm = null; if (configCopy.getMasterSlaveServersConfig() != null) { validate(configCopy.getMasterSlaveServersConfig()); cm = new MasterSlaveConnectionManager(configCopy.getMasterSlaveServersConfig(), serviceManager); } else if (configCopy.getSingleServerConfig() != null) { validate(configCopy.getSingleServerConfig()); // 我们指定的是单实例的 cm = new SingleConnectionManager(configCopy.getSingleServerConfig(), serviceManager); } else if (configCopy.getSentinelServersConfig() != null) { validate(configCopy.getSentinelServersConfig()); cm = new SentinelConnectionManager(configCopy.getSentinelServersConfig(), serviceManager); } else if (configCopy.getClusterServersConfig() != null) { validate(configCopy.getClusterServersConfig()); cm = new ClusterConnectionManager(configCopy.getClusterServersConfig(), serviceManager); } else if (configCopy.getReplicatedServersConfig() != null) { validate(configCopy.getReplicatedServersConfig()); cm = new ReplicatedConnectionManager(configCopy.getReplicatedServersConfig(), serviceManager); } else if (configCopy.getConnectionManager() != null) { cm = configCopy.getConnectionManager(); } if (cm == null) { throw new IllegalArgumentException("server(s) address(es) not defined!"); } // 获取连接 cm.connect(); return cm; } // --------------4 SingleConnectionManager public SingleConnectionManager(SingleServerConfig cfg, ServiceManager serviceManager) { // 调用 5-父类中的构造器 super(create(cfg), serviceManager); } // --------------5 MasterSlaveConnectionManager public MasterSlaveConnectionManager(BaseMasterSlaveServersConfig<?> cfg, ServiceManager serviceManager) { this.serviceManager = serviceManager; if (cfg instanceof MasterSlaveServersConfig) { this.config = (MasterSlaveServersConfig) cfg; if (this.config.getSlaveAddresses().isEmpty() && (this.config.getReadMode() == ReadMode.SLAVE || this.config.getReadMode() == ReadMode.MASTER_SLAVE)) { throw new IllegalArgumentException("Slaves aren't defined. readMode can't be SLAVE or MASTER_SLAVE"); } } else { this.config = create(cfg); } serviceManager.setConfig(this.config); // 这里是一个初始化timer的地方 serviceManager.initTimer(); subscribeService = new PublishSubscribeService(this); } // --------------6 ServiceManager public void initTimer() { int[] timeouts = new int[]{config.getRetryInterval(), config.getTimeout()}; Arrays.sort(timeouts); int minTimeout = timeouts[0]; if (minTimeout % 100 != 0) { minTimeout = (minTimeout % 100) / 2; } else if (minTimeout == 100) { minTimeout = 50; } else { minTimeout = 100; } // 这里是netty实现的时间轮,用于锁续期 timer = new HashedWheelTimer(new DefaultThreadFactory("redisson-timer"), minTimeout, TimeUnit.MILLISECONDS, 1024, false); connectionWatcher = new IdleConnectionWatcher(group, config); }
-
获取锁:redisson.getLock(“lock”)
// --------------1 Redisson public RLock getLock(String name) { return new RedissonLock(commandExecutor, name); } // --------------2 RedissonLock public RedissonLock(CommandAsyncExecutor commandExecutor, String name) { super(commandExecutor, name); this.commandExecutor = commandExecutor; this.internalLockLeaseTime = commandExecutor.getServiceManager().getCfg().getLockWatchdogTimeout(); this.pubSub = commandExecutor.getConnectionManager().getSubscribeService().getLockPubSub(); } // --------------3 RedissonBaseLock public RedissonBaseLock(CommandAsyncExecutor commandExecutor, String name) { // 调用父类,将name进行记录 super(commandExecutor, name); this.commandExecutor = commandExecutor; // 获取serviceManager里面设置的id:UUID.randomUUID().toString() this.id = commandExecutor.getServiceManager().getId(); // 获取config中设置的锁过期时间,默认值:30 * 1000 this.internalLockLeaseTime = commandExecutor.getServiceManager().getCfg().getLockWatchdogTimeout(); // 应用的uuid拼上锁名称 this.entryName = id + ":" + name; }
-
加锁:lock.lock()
// --------------1 RedissonLock public void lock() { try { lock(-1, null, false); } catch (InterruptedException e) { throw new IllegalStateException(); } } // --------------2 RedissonLock // leaseTime:锁过期时间 private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException { long threadId = Thread.currentThread().getId(); // 尝试获取锁,-1代表锁一直阻塞获取 Long ttl = tryAcquire(-1, leaseTime, unit, threadId); // lock acquired // 获取到锁了,直接返回 if (ttl == null) { return; } CompletableFuture<RedissonLockEntry> future = subscribe(threadId); pubSub.timeout(future); RedissonLockEntry entry; if (interruptibly) { entry = commandExecutor.getInterrupted(future); } else { entry = commandExecutor.get(future); } try { while (true) { ttl = tryAcquire(-1, leaseTime, unit, threadId); // lock acquired if (ttl == null) { break; } // waiting for message if (ttl >= 0) { try { entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { if (interruptibly) { throw e; } entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS); } } else { if (interruptibly) { entry.getLatch().acquire(); } else { entry.getLatch().acquireUninterruptibly(); } } } } finally { unsubscribe(entry, threadId); } // get(lockAsync(leaseTime, unit)); } // --------------3 RedissonLock private Long tryAcquire(long waitTime, long leaseTime, TimeUnit unit, long threadId) { return get(tryAcquireAsync(waitTime, leaseTime, unit, threadId)); } // --------------4 RedissonLock private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) { RFuture<Long> ttlRemainingFuture; // 判断是否设置了锁超时时间 if (leaseTime > 0) { // 如果业务方传了所过期时间,就用业务方自己传的 ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG); } else { // 没有设置,如-1,走这个代码,使用默认是的锁过期时间,internalLockLeaseTime:30 * 1000 // --------------5 RedissonLock ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime, TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG); } CompletionStage<Long> s = handleNoSync(threadId, ttlRemainingFuture); ttlRemainingFuture = new CompletableFutureWrapper<>(s); CompletionStage<Long> f = ttlRemainingFuture.thenApply(ttlRemaining -> { // lock acquired // 如果==null,那么就获取锁成功,否则获取锁失败 if (ttlRemaining == null) { if (leaseTime > 0) { // 如果业务方自己传了锁过期时间,那么internalLockLeaseTime就改成业务方自己传的 internalLockLeaseTime = unit.toMillis(leaseTime); } else { // 如果业务方没有传锁过期时间,默认锁过期时间为30s // 下面这个逻辑就是加锁成功后,watchDog在后台自动续期的逻辑 // --------------6 RedissonBaseLock scheduleExpirationRenewal(threadId); } } return ttlRemaining; }); return new CompletableFutureWrapper<>(f); } // --------------5 RedissonLock <T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) { return evalWriteAsync(getRawName(), LongCodec.INSTANCE, command, "if ((redis.call('exists', KEYS[1]) == 0) " + "or (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]);", // KEYS[1]=传进去的锁名称,ARGV[1]= 锁过期时间,ARGV[2]=id + ":" + threadId // id为uuid Collections.singletonList(getRawName()), unit.toMillis(leaseTime), getLockName(threadId)); } // --------------6 RedissonBaseLock protected void scheduleExpirationRenewal(long threadId) { ExpirationEntry entry = new ExpirationEntry(); // 享元模式,将当前线程需要续约对象放入的map中 // map的key=id + ":" + name(uuid+":"+锁名称) // putIfAbsent:如果map中存在就返回已有的,如果不存在将entry放进去,返回null ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry); if (oldEntry != null) { // 不等于null,将当前线程加入到续期对象中 oldEntry.addThreadId(threadId); } else { // 等于null,第一次加锁成功 entry.addThreadId(threadId); try { // 续约过期时间 // --------------7 RedissonBaseLock renewExpiration(); } finally { if (Thread.currentThread().isInterrupted()) { cancelExpirationRenewal(threadId); } } } } // --------------7 RedissonBaseLock private void renewExpiration() { // 从续约map中获取续约对象 ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName()); if (ee == null) { // 如果为空,则没有需要续约的对象,直接返回 return; } Timeout task = commandExecutor.getServiceManager().newTimeout(new TimerTask() { @Override public void run(Timeout timeout) throws Exception { ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName()); // 再次进行判断,如果为空,则没有需要续约的对象,直接返回 if (ent == null) { return; } Long threadId = ent.getFirstThreadId(); if (threadId == null) { return; } // 如果续约对象不为空,续约的线程id也不为空,进行续约 // --------------8 RedissonBaseLock CompletionStage<Boolean> future = renewExpirationAsync(threadId); future.whenComplete((res, e) -> { if (e != null) { log.error("Can't update lock {} expiration", getRawName(), e); // 如果续约出现异常,从续约map中移除当前的续约对象 EXPIRATION_RENEWAL_MAP.remove(getEntryName()); return; } if (res) { // reschedule itself // 如果续约成功,那么下一个时间轮接着执行一次锁续约 renewExpiration(); } else { // 如果续约失败,那么取消锁的续约操作 cancelExpirationRenewal(null); } }); } // 续约任务执行的时间间隔:30s/3=10s }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS); ee.setTimeout(task); } // --------------8 RedissonBaseLock protected CompletionStage<Boolean> renewExpirationAsync(long threadId) { return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " + "redis.call('pexpire', KEYS[1], ARGV[1]); " + "return 1; " + "end; " + "return 0;", // KEYS[1]=传入的锁名称,ARGV[1]=锁过期时间,ARGV[2]=id + ":" + threadId Collections.singletonList(getRawName()), internalLockLeaseTime, getLockName(threadId)); }
-
释放锁:lock.unlock()
// --------------1 RedissonBaseLock public void unlock() { try { get(unlockAsync(Thread.currentThread().getId())); } catch (RedisException e) { if (e.getCause() instanceof IllegalMonitorStateException) { throw (IllegalMonitorStateException) e.getCause(); } else { throw e; } } } // --------------2 RedissonBaseLock public RFuture<Void> unlockAsync(long threadId) { RFuture<Boolean> future = unlockInnerAsync(threadId); CompletionStage<Void> f = future.handle((opStatus, e) -> { cancelExpirationRenewal(threadId); if (e != null) { throw new CompletionException(e); } if (opStatus == null) { IllegalMonitorStateException cause = new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: " + id + " thread-id: " + threadId); throw new CompletionException(cause); } return null; }); return new CompletableFutureWrapper<>(f); } // --------------3 RedissonLock protected RFuture<Boolean> unlockInnerAsync(long threadId) { return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " + "return nil;" + "end; " + "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " + "if (counter > 0) then " + "redis.call('pexpire', KEYS[1], ARGV[2]); " + "return 0; " + "else " + "redis.call('del', KEYS[1]); " + "redis.call('publish', KEYS[2], ARGV[1]); " + "return 1; " + "end; " + "return nil;", // KEYS[1]=锁名称,KEYS[2]="redisson_lock__channel" + ":{" + name + "}" // ARGV[1]=0,ARGV[2]=internalLockLeaseTime,ARGV[3]=id + ":" + threadId Arrays.asList(getRawName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId)); }
-
阻塞获取锁
// --------------1 RedissonLock private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException { long threadId = Thread.currentThread().getId(); Long ttl = tryAcquire(-1, leaseTime, unit, threadId); // lock acquired if (ttl == null) { return; } // 订阅释放锁的channel,等待锁持有者释放锁后pub的消息 CompletableFuture<RedissonLockEntry> future = subscribe(threadId); pubSub.timeout(future); RedissonLockEntry entry; if (interruptibly) { entry = commandExecutor.getInterrupted(future); } else { entry = commandExecutor.get(future); } try { while (true) { // 再次尝试获取锁 ttl = tryAcquire(-1, leaseTime, unit, threadId); // lock acquired if (ttl == null) { // 获取锁成功,跳出循环 break; } // waiting for message if (ttl >= 0) { try { // 重要。通过Semaphore将没有获取锁的线程进行阻塞。阻塞时间为当前持有锁的剩余过期时间 // 这里的Semaphore设置的为0,目的是将获取锁失败的线程阻塞在这里。没有这一步,线程会 // 不断的循环,会造成CPU空转 entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { if (interruptibly) { throw e; } entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS); } } else { if (interruptibly) { entry.getLatch().acquire(); } else { entry.getLatch().acquireUninterruptibly(); } } } } finally { // 循环结束,说明当前线程已经获取到锁,就取消消息订阅 unsubscribe(entry, threadId); } }
-
非阻塞获取锁
// --------------1 RedissonLock @Override public boolean tryLock(long waitTime, TimeUnit unit) throws InterruptedException { return tryLock(waitTime, -1, unit); } // --------------2 RedissonLock @Override public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException { // 需要阻塞获取锁的时间 long time = unit.toMillis(waitTime); // 当前时间 long current = System.currentTimeMillis(); long threadId = Thread.currentThread().getId(); // 尝试获取锁,ttl为别的线程持有锁的剩余过期时间 Long ttl = tryAcquire(waitTime, leaseTime, unit, threadId); // lock acquired if (ttl == null) { // 如果为null,说明获取锁成功,直接返回 return true; } // 剩余阻塞时间。总的阻塞时间-(获取锁消耗的时间(当前时间-上一次记录的当前时间)) time -= System.currentTimeMillis() - current; if (time <= 0) { // 剩余阻塞时间小于等于0,获取锁失败,进行返回 acquireFailed(waitTime, unit, threadId); return false; } // 重置上一次的当前时间变量 current = System.currentTimeMillis(); // 订阅锁释放的消息 CompletableFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId); try { subscribeFuture.get(time, TimeUnit.MILLISECONDS); } catch (TimeoutException e) { if (!subscribeFuture.completeExceptionally(new RedisTimeoutException( "Unable to acquire subscription lock after " + time + "ms. " + "Try to increase 'subscriptionsPerConnection' and/or 'subscriptionConnectionPoolSize' parameters."))) { subscribeFuture.whenComplete((res, ex) -> { if (ex == null) { unsubscribe(res, threadId); } }); } acquireFailed(waitTime, unit, threadId); return false; } catch (ExecutionException e) { acquireFailed(waitTime, unit, threadId); return false; } try { // 剩余阻塞时间。上一次剩余的阻塞时间-(订阅消息耗费的时间(当前时间-上一次记录的当前时间)) time -= System.currentTimeMillis() - current; if (time <= 0) { acquireFailed(waitTime, unit, threadId); return false; } while (true) { long currentTime = System.currentTimeMillis(); // 再次尝试获取锁 ttl = tryAcquire(waitTime, leaseTime, unit, threadId); // lock acquired if (ttl == null) { // 如果为null,说明获取锁成功,直接返回 return true; } // 剩余阻塞时间。总的阻塞时间-(获取锁消耗的时间(当前时间-上一次记录的当前时间)) time -= System.currentTimeMillis() - currentTime; if (time <= 0) { acquireFailed(waitTime, unit, threadId); return false; } // waiting for message // 重置当前时间 currentTime = System.currentTimeMillis(); // 这一步操作很优美。尽量让当前线程阻塞最少的时间 if (ttl >= 0 && ttl < time) { // 如果别的线程持有锁的剩余过期时间小于获取锁的阻塞时间,就让当前线程阻塞ttl时间 commandExecutor.getNow(subscribeFuture).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS); } else { // 如果别的线程持有锁的剩余过期时间大于获取锁的阻塞时间,就让当前线程阻塞time时间 commandExecutor.getNow(subscribeFuture).getLatch().tryAcquire(time, TimeUnit.MILLISECONDS); } // 计算锁的剩余阻塞时间 time -= System.currentTimeMillis() - currentTime; if (time <= 0) { acquireFailed(waitTime, unit, threadId); return false; } } } finally { unsubscribe(commandExecutor.getNow(subscribeFuture), threadId); } // return get(tryLockAsync(waitTime, leaseTime, unit)); }
公平锁
-
加锁逻辑:lock.tryLockInnerAsync(5000, leaseTime, TimeUnit.MILLISECONDS, threadInit, RedisCommands.EVAL_LONG)
// waitTime:锁等待超时时间 // leaseTime:锁过期时间 // command:执行的命令,这里是EVAL_LONG @Override <T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) { // 获取线程等待时间:60000*5=300s=5min long wait = threadWaitTime; if (waitTime > 0) { wait = unit.toMillis(waitTime); } // 当前时间 long currentTime = System.currentTimeMillis(); if (command == RedisCommands.EVAL_NULL_BOOLEAN) { return evalWriteAsync(getRawName(), LongCodec.INSTANCE, command, // remove stale threads "while true do " + "local firstThreadId2 = redis.call('lindex', KEYS[2], 0);" + "if firstThreadId2 == false then " + "break;" + "end;" + "local timeout = tonumber(redis.call('zscore', KEYS[3], firstThreadId2));" + "if timeout <= tonumber(ARGV[3]) then " + // remove the item from the queue and timeout set // NOTE we do not alter any other timeout "redis.call('zrem', KEYS[3], firstThreadId2);" + "redis.call('lpop', KEYS[2]);" + "else " + "break;" + "end;" + "end;" + "if (redis.call('exists', KEYS[1]) == 0) " + "and ((redis.call('exists', KEYS[2]) == 0) " + "or (redis.call('lindex', KEYS[2], 0) == ARGV[2])) then " + "redis.call('lpop', KEYS[2]);" + "redis.call('zrem', KEYS[3], ARGV[2]);" + // decrease timeouts for all waiting in the queue "local keys = redis.call('zrange', KEYS[3], 0, -1);" + "for i = 1, #keys, 1 do " + "redis.call('zincrby', KEYS[3], -tonumber(ARGV[4]), keys[i]);" + "end;" + "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 1;", Arrays.asList(getRawName(), threadsQueueName, timeoutSetName), unit.toMillis(leaseTime), getLockName(threadId), currentTime, wait); } if (command == RedisCommands.EVAL_LONG) { return evalWriteAsync(getRawName(), LongCodec.INSTANCE, command, // remove stale threads // 移除不新鲜的线程id,移除过期的线程 // "while true do " + "local firstThreadId2 = redis.call('lindex', KEYS[2], 0);" + "if firstThreadId2 == false then " + // list中没有等待线程,直接跳出循环 "break;" + "end;" + // 从zset中获取等待线程的超时时间 "local timeout = tonumber(redis.call('zscore', KEYS[3], firstThreadId2));" + // 判断当前线程是否已经超时 "if timeout <= tonumber(ARGV[4]) then " + // remove the item from the queue and timeout set // NOTE we do not alter any other timeout // 超时就从list和zset中移除等待线程 "redis.call('zrem', KEYS[3], firstThreadId2);" + "redis.call('lpop', KEYS[2]);" + "else " + "break;" + "end;" + "end;" + // check if the lock can be acquired now,检查当前这把锁是否可以被获取到 // 可以加锁的判断逻辑: // 当前不存在锁 and (当前不存在等待锁的线程 or 存在等待锁的线程,但是就是本次需要 加锁的线程) "if (redis.call('exists', KEYS[1]) == 0) " + "and ((redis.call('exists', KEYS[2]) == 0) " + "or (redis.call('lindex', KEYS[2], 0) == ARGV[2])) then " + // remove this thread from the queue and timeout set // 移除list和zset中等待的线程 "redis.call('lpop', KEYS[2]);" + "redis.call('zrem', KEYS[3], ARGV[2]);" + // decrease timeouts for all waiting in the queue // 获取zset中等待的线程 "local keys = redis.call('zrange', KEYS[3], 0, -1);" + "for i = 1, #keys, 1 do " + // 将所有的等待线程的等待时间减300s,// todo "redis.call('zincrby', KEYS[3], -tonumber(ARGV[3]), keys[i]);" + "end;" + // acquire the lock and set the TTL for the lease "redis.call('hset', KEYS[1], ARGV[2], 1);" + "redis.call('pexpire', KEYS[1], ARGV[1]);" + "return nil;" + "end;" + // check if the lock is already held, and this is a re-entry // 判断此次加锁的线程是否是当前持有锁的线程,如果是就把可重入次数+1,再次设置一个超 时时间 "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;" + // the lock cannot be acquired // check if the thread is already in the queue // 走到这里,说明前面加锁失败,锁已经被其它线程持有 // 判断加锁线程是否已经在zset中存在了,存在说明之前已经尝试过一次加锁 "local timeout = redis.call('zscore', KEYS[3], ARGV[2]);" + "if timeout ~= false then " + // the real timeout is the timeout of the prior thread // in the queue, but this is approximately correct, and // avoids having to traverse the queue // 返回 timeout-wait-current // todo "return timeout - tonumber(ARGV[3]) - tonumber(ARGV[4]);" + "end;" + // add the thread to the queue at the end, and set its timeout in the timeout set to the timeout of // the prior thread in the queue (or the timeout of the lock if the queue is empty) plus the // threadWaitTime // 从list中获取最后一个元素 "local lastThreadId = redis.call('lindex', KEYS[2], -1);" + "local ttl;" + // 如果list里面有元素,说明等待队列中已经有其他线程了 "if lastThreadId ~= false and lastThreadId ~= ARGV[2] then " + // ttl=等待队列中最后一个线程的等待时间-当前时间 "ttl = tonumber(redis.call('zscore', KEYS[3], lastThreadId)) - tonumber(ARGV[4]);" + "else " + // ttl=当前锁的剩余过期时间 "ttl = redis.call('pttl', KEYS[1]);" + "end;" + // 计算当前线程的等待时间=ttl+300s+当前时间 "local timeout = ttl + tonumber(ARGV[3]) + tonumber(ARGV[4]);" + "if redis.call('zadd', KEYS[3], timeout, ARGV[2]) == 1 then " + "redis.call('rpush', KEYS[2], ARGV[2]);" + "end;" + "return ttl;", // KEYS[1]:锁名称 KEYS[2]:redisson_lock_queue+[锁名称] KEYS[3]: redisson_lock_timeout+[锁名称] // redisson_lock_queue+[锁名称]:list数据结构,里面存储的是加锁失败的线程id, rpush进去的 // redisson_lock_timeout+[锁名称]:zset数据结构,里面存储的是加锁失败的线程id, score存储的是超时时间 // ARGV[1]:锁过期时间 ARGV[2]:id + ":" + threadId ARGV[3]: 60000*5=300s=5min ARGV[4]:当前时间 Arrays.asList(getRawName(), threadsQueueName, timeoutSetName), unit.toMillis(leaseTime), getLockName(threadId), wait, currentTime); } throw new IllegalArgumentException(); }
-
释放锁:lock.unlockInnerAsync(threadInit)
@Override protected RFuture<Boolean> unlockInnerAsync(long threadId) { return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, // remove stale threads // 移除等待队列中已经过期的等待线程 "while true do " + "local firstThreadId2 = redis.call('lindex', KEYS[2], 0);" + "if firstThreadId2 == false then " + "break;" + "end; " + "local timeout = tonumber(redis.call('zscore', KEYS[3], firstThreadId2));" + "if timeout <= tonumber(ARGV[4]) then " + "redis.call('zrem', KEYS[3], firstThreadId2); " + "redis.call('lpop', KEYS[2]); " + "else " + "break;" + "end; " + "end;" // 如果当前没有线程持有锁 + "if (redis.call('exists', KEYS[1]) == 0) then " + "local nextThreadId = redis.call('lindex', KEYS[2], 0); " + "if nextThreadId ~= false then " + // 获取等待队列中的第一元素,发布消息通知 "redis.call('publish', KEYS[4] .. ':' .. nextThreadId, ARGV[1]); " + "end; " + "return 1; " + "end;" + // 如果持有锁的线程不是当前需要释放锁的线程,直接返回 "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " + "return nil;" + "end; " + // 走到这里说明当前持有锁的线程是需要释放锁的线程,对加锁次数减1 "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " + "if (counter > 0) then " + // 判断加锁次数,大于0就对锁过期时间重置 "redis.call('pexpire', KEYS[1], ARGV[2]); " + "return 0; " + "end; " + // 如果加锁次数减到1,就删除锁 "redis.call('del', KEYS[1]); " + "local nextThreadId = redis.call('lindex', KEYS[2], 0); " + "if nextThreadId ~= false then " + // 删除锁以后,如果等待队列中还有线程,就发布消息通知 "redis.call('publish', KEYS[4] .. ':' .. nextThreadId, ARGV[1]); " + "end; " + "return 1; ", Arrays.asList(getRawName(), threadsQueueName, timeoutSetName, getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId), System.currentTimeMillis()); }
MultiLock
-
简单案例演示
@Test public void testMultiLock() { Config config = new Config(); config.useSingleServer() .setAddress("redis://127.0.0.1:6379") .setDatabase(1) .setPassword("^Koron#3438$"); RedissonClient redissonClient = Redisson.create(config); RLock lock1 = redissonClient.getLock("lock1"); RLock lock2 = redissonClient.getLock("lock2"); RLock lock3 = redissonClient.getLock("lock3"); RedissonMultiLock multiLock = new RedissonMultiLock(lock1, lock2, lock3); multiLock.lock(); multiLock.unlock(); }
-
使用场景
下订单操作:之前我们的下订单示例是对每一个商品加一把锁。但是往往一个订单中会有多个商品,这里我们就可以锁定多个商品
-
加锁逻辑
// --------- 1 RedissonMultiLock @Override public void lock() { try { lockInterruptibly(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } // --------- 2 RedissonMultiLock @Override public void lockInterruptibly() throws InterruptedException { lockInterruptibly(-1, null); } // --------- 3 RedissonMultiLock @Override public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException { // 获取基础的等待时间:3*1500 long baseWaitTime = locks.size() * 1500; // 循环加锁 while (true) { long waitTime; // 如果锁过期时间小于0 if (leaseTime <= 0) { // 锁等待时间为:3*1500 waitTime = baseWaitTime; } else { // 否则锁等待时间等于锁过期时间 waitTime = unit.toMillis(leaseTime); if (waitTime <= baseWaitTime) { waitTime = ThreadLocalRandom.current().nextLong(waitTime/2, waitTime); } else { waitTime = ThreadLocalRandom.current().nextLong(baseWaitTime, waitTime); } } if (tryLock(waitTime, leaseTime, TimeUnit.MILLISECONDS)) { return; } } } // --------- 4 RedissonMultiLock @Override public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException { long newLeaseTime = -1; if (leaseTime > 0) { if (waitTime > 0) { newLeaseTime = unit.toMillis(waitTime)*2; } else { newLeaseTime = unit.toMillis(leaseTime); } } // 当前时间 long time = System.currentTimeMillis(); long remainTime = -1; if (waitTime > 0) { // 剩余时间:3*1500 remainTime = unit.toMillis(waitTime); } // 计算锁等待时间 long lockWaitTime = calcLockWaitTime(remainTime); // 允许加锁失败的次数,mutilLock默认为0 int failedLocksLimit = failedLocksLimit(); // 加锁成功的容器 List<RLock> acquiredLocks = new ArrayList<>(locks.size()); // 遍历所有的锁 for (ListIterator<RLock> iterator = locks.listIterator(); iterator.hasNext();) { RLock lock = iterator.next(); boolean lockAcquired; try { if (waitTime <= 0 && leaseTime <= 0) { // 尝试获取锁 lockAcquired = lock.tryLock(); } else { // 尝试在指定等待时间内获取锁 long awaitTime = Math.min(lockWaitTime, remainTime); lockAcquired = lock.tryLock(awaitTime, newLeaseTime, TimeUnit.MILLISECONDS); } } catch (RedisResponseTimeoutException e) { // 出现异常,把当前加锁的这个释放掉 unlockInner(Arrays.asList(lock)); // 设置结果为false lockAcquired = false; } catch (Exception e) { lockAcquired = false; } if (lockAcquired) { // 如果加锁成功,将当前的锁放到加锁成功的容器中 acquiredLocks.add(lock); } else { // 如果加锁失败,判断加锁的数量是否等于加锁成功的数量,如果是的跳出循环 if (locks.size() - acquiredLocks.size() == failedLocksLimit()) { break; } // 只要有一个加锁失败,就把之前加锁成功的释放掉 if (failedLocksLimit == 0) { unlockInner(acquiredLocks); // 如果锁等待时间小于0以后,就直接返回 if (waitTime <= 0) { return false; } failedLocksLimit = failedLocksLimit(); // 清空加锁成功的容器 acquiredLocks.clear(); // reset iterator // 重置迭代器到上一个锁 while (iterator.hasPrevious()) { iterator.previous(); } } else { failedLocksLimit--; } } // 重新计算剩余时间 if (remainTime > 0) { remainTime -= System.currentTimeMillis() - time; time = System.currentTimeMillis(); if (remainTime <= 0) { unlockInner(acquiredLocks); return false; } } } if (leaseTime > 0) { acquiredLocks.stream() .map(l -> (RedissonBaseLock) l) .map(l -> l.expireAsync(unit.toMillis(leaseTime), TimeUnit.MILLISECONDS)) .forEach(f -> f.toCompletableFuture().join()); } return true; }