如何使用Redission实现分布式锁及幂等性?【AOP+工具类的两种解决方案】
目录
分布式Id生成器:分布式ID生成器-CSDN博客
https://blog.csdn.net/2302_79840586/article/details/156598154?spm=1001.2014.3001.5501
整体架构图:
┌─────────────────────────────────────────────────────────────┐
│ 使用层 (User Layer) │
│ ┌─────────────────────┐ ┌──────────────────────────┐ │
│ │ 注解式 (Declarative)│ │ 编程式 (Programmatic) │ │
│ │ @ServiceLock │ │ ServiceLockTool.execute()│ │
│ └──────────┬───────────┘ └────────────┬─────────────┘ │
└─────────────┼────────────────────────────────┼────────────────┘
│ │
▼ ▼
┌─────────────────────────────────────────────────────────────┐
│ AOP切面层 (Aspect Layer) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ ServiceLockAspect (@Order(-10)) │ │
│ │ - 拦截 @ServiceLock 注解 │ │
│ │ - 解析 SpEL 表达式 │ │
│ │ - tryLock() → proceed() → unlock() │ │
│ └───────────────────────┬──────────────────────────────┘ │
└────────────────────────────┼───────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 工厂层 (Factory Layer) │
│ ┌──────────────────────┐ ┌─────────────────────────┐ │
│ │ ServiceLockFactory │──────│ ManageLocker (缓存) │ │
│ │ - getLock(LockType) │ │ - Map<LockType, Lock> │ │
│ └──────────┬───────────┘ └─────────────────────────┘ │
└─────────────┼──────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 实现层 (Implementation Layer) │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ServiceLocker接口 │ │ 4种具体实现 │ │
│ │- lock() │ │ RedissonReentrant│ │
│ │- tryLock() │◄─┤ RedissonFair │ │
│ │- unlock() │ │ RedissonRead │ │
│ │- getLock() │ │ RedissonWrite │ │
│ └──────────────────┘ └──────────┬───────┘ │
└──────────────────────────────────────┼─────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Redisson 客户端层 (Redisson Client) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ RedissonClient │ │
│ │ - getLock(key) → RLock │ │
│ │ - getFairLock(key) → RLock │ │
│ │ - getReadWriteLock(key) → RReadWriteLock │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
[ Redis 服务器 ]
Redission分布式锁原理
for update
select column from table where column = ... for update
在 select 的 sql 上加上 for update 会对此记录加上行级锁,在超时,提交,回滚会进行释放。
缺点:
- 当请求等待锁释放时,不能灵活的控制加锁时间、等待锁的时间
- 如果在一个事务中,开始的时候就使用for update的话,则需要这个事务执行完提交或回滚才能够解锁,不能很好的控制锁的粒度,并发性会降低。
- 在Repeatable Read的隔离级别下有可能会产生死锁。https://www.cnblogs.com/micrari/p/8029710.html
https://www.cnblogs.com/micrari/p/8029710.html
Redis锁
public ResultMap<IDCardOCRVo> IDCardOCR(IDCardOCRDto dto){
//部分省略。。。
//通过redis防重提交
Boolean ifAbsent = stringRedisTemplate.opsForValue().setIfAbsent(userId, "1");
if (ifAbsent) {
stringRedisTemplate.expire(userId, 15, TimeUnit.SECONDS);
}else {
throw new BusinessException(ResultCode.NOT_FREQUENTLY_OPERATE);
}
}
如果执行到if (ifAbsent)服务挂掉,那么这个userId就会一直存在redis中,别的请求一直获取不到,相当于死锁。
Redission
Redisson 是架设在 Redis 基础上的一个 Java 驻内存数据网格框架,充分利用 Redis 键值数据库提供的一系列优势,基于 Java 实用工具包中常用接口,为使用者提供了 一系列具有分布式特性的常用工具类。
- 指定一个 key 作为锁标记,存入 Redis 中,指定一个 唯一的用户标识 作为 value。
- 当 key 不存在时才能设置值,确保同一时间只有一个客户端进程获得锁,满足 互斥性 特性。
- 设置一个过期时间,防止因系统异常导致没能删除这个 key,满足 防死锁 特性。
- 当处理完业务之后需要清除这个 key 来释放锁,清除 key 时需要校验 value 值,需要满足 只有加锁的人才能释放锁。
- WatchDog 机制 能够很好的解决锁续期的问题,预防死锁。
- 能够灵活的设置加锁时间,等待锁时间,释放锁失败后锁的存在时间。

现在来说,redis 分布式锁,redisson 去加锁,也就是去 redis 集群中选择一台 master 实例去实现锁机制,并且能因为一台 master 可能会挂载多台 slave 实例,这样也就实现了高可用性。
但是呢,不得不去思考,如果 master 和 salve 同步的过程中,master宕机了,偏偏在这之前某个服务实例刚刚写入了一把锁,这时候就尴尬了,salve 还没有同步到这把锁,就被切换成了 master,那么这时候可以说就有问题了,
另一个服务实例在新的 master 上获取到一把新锁,这时候就会出现俩台服务实例都持有锁,执行业务逻辑的场景,这个是有问题的。也是在生产环境中我们需要去考虑的一个问题。
使用 AOP 实现分布式锁
1.根据环境获取环境名:
下面是一个 String 工具类,主要用于去获得
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
public class SpringUtil implements ApplicationContextInitializer<ConfigurableApplicationContext> {
public static final String PREFIX_DISTINCTION_NAME = "prefix.distinction.name";
public static final String DEFAULT_PREFIX_DISTINCTION_NAME = "smart";
private static ConfigurableApplicationContext configurableApplicationContext;
// key: {环境前缀}
public static String getPrefixDistinctionName(){
return configurableApplicationContext.getEnvironment().getProperty(PREFIX_DISTINCTION_NAME,
DEFAULT_PREFIX_DISTINCTION_NAME);
}
// 在 Spring 启动初期,把整个 Spring 容器 “托管” 到一个全局静态变量里
@Override
public void initialize(final ConfigurableApplicationContext applicationContext) {
configurableApplicationContext = applicationContext;
}
}
那为什么要实现 ApplicationContextInitializer ?
public class SpringUtil
implements ApplicationContextInitializer<ConfigurableApplicationContext> {
这是因为 ApplicationContextInitializer 的执行时机是 Spring 容器创建完成,但还没 refresh 之前。也就是说此时 Environment 已经就绪 而 Bean 还没创建,所以非常适合做 “全局上下文初始化”。
然后假设在 application.yml 内配置:
prefix:
distinction:
name: prod-smart-user
那么 Spring 会自动解析成:prefix.distinction.name = prod-smart-user
而如果什么都没写,那么 Spring 就会使用兜底的默认值 也就是上面配置的 smart:
public static final String DEFAULT_PREFIX_DISTINCTION_NAME = "smart";
2.获取锁名的方法:
先将 String 常用字符串方法封装:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
/*
* String工具
*/
public class StringUtil {
private final static Logger logger = LoggerFactory.getLogger(StringUtil.class);
/**
* 判断字符串不为空
* @param str 字符串
* @return
*/
public static boolean isNotEmpty(String str) {
return (str != null && !str.isEmpty() && !str.trim().isEmpty() && !"null".equalsIgnoreCase(str.trim())
&& !"undefined".equalsIgnoreCase(str.trim()) && !"NULL".equalsIgnoreCase(str.trim()));
}
/**
* 判断字符串为空
* @param str 字符串
* @return
*/
public static boolean isEmpty(String str) {
return !StringUtil.isNotEmpty(str);
}
/**
* 将流转换为字符串
* @param is 文件流
* @return
*/
public static String inputStreamConvertString(InputStream is){
ByteArrayOutputStream baos = null;
String result = null;
try {
if(is != null) {
baos = new ByteArrayOutputStream();
int i;
while ((i = is.read()) != -1) {
baos.write(i);
}
result = baos.toString();
}
}catch(IOException e) {
throw new RuntimeException("流转换为字符串失败!");
}finally {
if(baos != null) {
try {
baos.close();
} catch (IOException e) {
logger.error("关闭流失败!");
}
}
}
return result;
}
/**
* 将URL参数转成map
* */
public static Map<String, String> convertQueryStringToMap(String queryString) {
Map<String, String> resultMap = new HashMap<>(256);
String[] params = queryString.split("&");
for (String param : params) {
String[] keyValue = param.split("=");
if (keyValue.length == 2) {
try {
String key = java.net.URLDecoder.decode(keyValue[0], "UTF-8");
String value = java.net.URLDecoder.decode(keyValue[1], "UTF-8");
resultMap.put(key, value);
} catch (Exception e) {
e.printStackTrace();
}
}
}
return resultMap;
}
}
下面方法是根据name以及传递过来的key相关的参数数组拼接而成的 key 名:
private static final String LOCK_DISTRIBUTE_ID_NAME_PREFIX = "LOCK_DISTRIBUTE_ID";
public static final String SEPARATOR = ":";
public String simpleGetLockName(String name,String[] keys){
// 存储有效的业务参数 Key
List<String> definitionKeyList = new ArrayList<>();
for (String key : keys) {
if (StringUtil.isNotEmpty(key)) {
definitionKeyList.add(key);
}
}
return SpringUtil.getPrefixDistinctionName() + "-" +
LOCK_DISTRIBUTE_ID_NAME_PREFIX + SEPARATOR + name + SEPARATOR + String.join(SEPARATOR, definitionKeyList);
}
假设 name = register,keys = ["13812345678","work"],
那么生成的 key 为 "prod-smart-user-LOCK_DISTRIBUTE_ID:register:13812345678:work"。
3.使用 AOP 实现分布式锁调度:
为简单便捷添加使用分布式锁,我们使用 Redisson+自定义注解 + Spring的AOP 的方式实现。
(1)事务切面 与 锁切面 的先后执行顺序导致的问题
举个例子,有一场景需根据 id 查出 test 对象后,在原有基础上增加 number 值,然后更新到数据库中:
@Transactional
@ServiceLock(name = "insertNumber",keys = {"#id"},waitTime = 50)
public boolean insertNumber(final Long number, final Long id) {
Test test = testMapper.getById(id);
Long originalNumber = test.getNumber();
originalNumber = originalNumber + number;
test.setNumber(originalNumber);
testMapper.updateById(test);
return true;
}
肯定需要保证事务,同时还需要加锁,然后再编写锁切面:
@Aspect
public class ServiceLockAspect {
@Around("@annotation(servicelock)")
public Object around(ProceedingJoinPoint joinPoint, ServiceLock servicelock) throws Throwable {
//解析出加锁的键
String lockName = (joinPoint,servicelock);
//进行加锁
boolean reuslt = lock.lock(lockName);
//如果加锁成功
if (reuslt) {
try {
//执行业务逻辑
return joinPoint.proceed();
}finally{
//解锁
lock.unlock(lockName);
}
}else {
//等待或者执行加锁失败的处理逻辑
}
}
}
在实际压测中发现并不能保证数据的正确性,比如设置一秒内发出1000个请求,参数 number 为1,正确结果应该是1000,但实际结果要比1000小。
由于事务的本质也是个切面,如果在 service 方法加锁的话,这时也就是该方法上同时存在 锁的切面 和 事务的切面,Spring 会将事务的切面和锁的切面放在一个切面 有序集合 中,然后依次的执行,这其实也是责任链模式。
在底层,Spring 是通过下面方法强调事务与锁的顺序:
protected List<Advisor> findCandidateAdvisors() {
//这里从父类找到了事务切面,并放入到advisors中
List<Advisor> advisors = super.findCandidateAdvisors();
// Build Advisors for all AspectJ aspects in the bean factory.
if (this.aspectJAdvisorsBuilder != null) {
//这里找到了锁的切面,放入到advisors
advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors());
}
return advisors;
}
可以看出先是父类找到了事务的切面放到了advisors集合中,后来又找到了锁的切面也放到了advisors集合中,而后续的sortAdvisors方法是针对@order值来排序,而事务和锁的切面都为 Integer.MAX_VALUE。
过程分析:先将所有切面加载到一个域成员变量的 Map 缓存中,加载时会将每个切面类中的切面方法进行排序,然后根据切面注解触发的顺序排序,之后根据字母序进行排序,这是整个初始化过程,初始化结束后每个切面类中的切面方法的顺序就不会再次改变了。而每次调用切面命中的业务代码时,就会触发切面扫描来筛选出匹配的切面方法,根据切面方法所在的切面类,通过 order 属性的值做一次排序,这次排序不会更改之前同一个类型中切面方法的相对顺序,根据上边几步的排序结果,依次触发切面的逻辑。
总而言之,当 业务切面 和 事务切面 都存在的话,如果不指定 order 的值,那么事务切面的执行顺序始终都会先于业务切面,不会按照切面名字来排序。
,所以我们就需要使用 @Order 注解让锁的切面的顺序先于事务,达到将锁放到事务外的目的,以此来保证整个事务也被锁住。
而由于事务的 order 值默认为 Integer.MAX_VALUE,考虑到后续可能还要用到切面功能,也需要在锁切面的里面,所以这里设置为-10:
@Aspect
@Order(-10)
public class ServiceLockAspect {
//省略
}
总结:
对于 事务和业务切面的执行顺序问题 导致了锁的范围没有将整个事务包裹住,我们可以通过两种方法解决:
- 将锁的切面放在 controller 的方法上,这样锁的切面肯定会先于事务切面执行
- 如果锁的切面和事务切面在一个方法上,那么指定锁切面的 order 值,比事务切面 order 值小即可(事务切面order默认为Integer.MAX_VALUE)
而对于锁的方法级使用:
lock.lock(lockName);
try {
//执行逻辑
}finally {
lock.unLock(lockName);
}
一般这都是在service层进行加锁的,所以出现的问题和上述切面问题相同,都是发生了事务切面在锁的切面之前执行,导致锁没有把事务包裹住。所以使用这种方法级别的分布式锁,要考虑在 Controller 控制层加锁或者设计出加锁层,在控制层和service层中间,当加锁后,再调用service的方法。
(2)设计一个 @ServiceLock 注解
注解 @ServiceLock 加在 Service 层的方法上即可,锁的键可以根据入参的多个参数一起配置,这样比较灵活,如果使用用户 Token 的话锁的粒度太大。而对于锁的参数主要有以下:
- 锁的类型包含可重入锁(默认)、公平锁、读锁、写锁
- 可配置锁的等待时间
- 提供了加锁失败后直接拒绝(默认)和自定义处理的两种处理方式
首先编写一个枚举类来列出锁的四种类型:
/*
* 分布式锁 锁类型
*/
public enum LockType {
/**
* 可重入锁
*/
Reentrant,
/**
* 公平锁
*/
Fair,
/**
* 读锁
*/
Read,
/**
* 写锁
*/
Write;
LockType() {
}
}
之后编写 @ServiceLock 注解:
四种锁的区别:
锁类型 概念 特性 典型应用场景 注意事项 Reentrant(可重入锁) 同一线程可以重复获得锁 - 单个线程可重复加锁- 避免死锁- 不保证公平 - 保护临界区- 单实例服务的并发控制 - 不保证加锁顺序- 可能造成线程饥饿 Fair(公平锁) 按请求顺序分配锁 - 先来先得- 阻止线程饥饿 - 高并发队列处理- 任务顺序敏感场景 - 性能略低于非公平锁- 竞争激烈时延迟增加 Read(读锁) 多线程可以同时读,但写时必须独占 - 多个线程可同时读- 写操作会被阻塞 - 缓存读多写少场景- 数据共享访问 - 写线程可能被长时间阻塞- 需要配合 Write 锁使用 Write(写锁) 独占锁,读写都需要等待 - 写操作独占- 阻止读线程和其他写线程 - 数据修改操作- 必须保证一致性 - 写锁获取可能被阻塞- 并发性能低于读锁
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;
/*
* 分布式锁 注解
*/
@Target(value= {ElementType.TYPE, ElementType.METHOD})
@Retention(value= RetentionPolicy.RUNTIME)
public @interface ServiceLock {
/**
* 锁的类型(默认 可重入锁)
* */
LockType lockType() default LockType.Reentrant;
/**
* 业务名称
* @return name
*/
String name() default "";
/**
* 自定义业务key
* @return keys
*/
String [] keys();
/**
* 尝试加锁失败最多等待时间
* @return waitTime
*/
long waitTime() default 10;
/**
* 时间单位
* @return TimeUnit
*/
TimeUnit timeUnit() default TimeUnit.SECONDS;
/**
* 加锁超时的处理策略
* @return LockTimeOutStrategy
*/
LockTimeOutStrategy lockTimeoutStrategy() default LockTimeOutStrategy.FAIL;
/**
* 自定义加锁超时的处理策略
* @return customLockTimeoutStrategy
*/
String customLockTimeoutStrategy() default "";
}
(3)编写自动配置类
ServiceLockAutoConfiguration是自动装配类,加载了 分布式锁的key解析处理器、分布式锁工厂、分布式锁切面、分布式锁工具的对象。
| Bean 名称 | 类型 | 作用 | 依赖 | 使用场景 |
|---|---|---|---|---|
serviceLockInfoHandle |
LockInfoHandle(实现类:ServiceLockInfoHandle) |
分布式锁 Key 的解析器,负责把方法参数、注解信息解析成唯一的锁 Key | 无 | 当 AOP 切面需要生成锁 Key 时调用 |
manageLocker |
ManageLocker |
分布式锁管理器,对 Redis 锁的获取、释放进行统一封装 | RedissonClient |
所有业务加锁、解锁操作,底层直接操作 Redis |
serviceLockFactory |
ServiceLockFactory |
分布式锁工厂,生成具体锁对象 | ManageLocker |
切面或工具类创建锁实例(可重入锁、读写锁等) |
serviceLockAspect |
ServiceLockAspect |
分布式锁切面,拦截 @ServiceLock 注解的方法 |
LockInfoHandleFactory + ServiceLockFactory |
业务方法执行前加锁,执行后释放锁,透明给业务 |
serviceLockUtil |
ServiceLockTool |
分布式锁工具类,非注解方式手动加锁 | LockInfoHandleFactory + ServiceLockFactory |
业务层直接调用工具类加锁,适合动态 Key 或非注解场景 |
public class ServiceLockAutoConfiguration {
/**
* 分布式锁的key解析处理器
* */
@Bean(LockInfoType.SERVICE_LOCK)
public LockInfoHandle serviceLockInfoHandle(){
return new ServiceLockInfoHandle();
}
/**
* 锁管理
* */
@Bean
public ManageLocker manageLocker(RedissonClient redissonClient){
return new ManageLocker(redissonClient);
}
/**
* 锁工厂
* */
@Bean
public ServiceLockFactory serviceLockFactory(ManageLocker manageLocker){
return new ServiceLockFactory(manageLocker);
}
/**
* 分布式锁切面
* */
@Bean
public ServiceLockAspect serviceLockAspect(LockInfoHandleFactory lockInfoHandleFactory,ServiceLockFactory serviceLockFactory){
return new ServiceLockAspect(lockInfoHandleFactory,serviceLockFactory);
}
/**
* 分布式锁工具
* */
@Bean
public ServiceLockTool serviceLockUtil(LockInfoHandleFactory lockInfoHandleFactory,ServiceLockFactory serviceLockFactory){
return new ServiceLockTool(lockInfoHandleFactory,serviceLockFactory);
}
}
(4)锁管理缓存对象实例
锁管理缓存,这里将重入锁、公平锁、写锁、读锁的实例给缓存起来,方便后序使用:
public class ManageLocker {
private final Map<LockType, ServiceLocker> cacheLocker = new HashMap<>();
public ManageLocker(RedissonClient redissonClient){
cacheLocker.put(Reentrant,new RedissonReentrantLocker(redissonClient));
cacheLocker.put(Fair,new RedissonFairLocker(redissonClient));
cacheLocker.put(Write,new RedissonWriteLocker(redissonClient));
cacheLocker.put(Read,new RedissonReadLocker(redissonClient));
}
public ServiceLocker getReentrantLocker(){
return cacheLocker.get(Reentrant);
}
public ServiceLocker getFairLocker(){
return cacheLocker.get(Fair);
}
public ServiceLocker getWriteLocker(){
return cacheLocker.get(Write);
}
public ServiceLocker getReadLocker(){
return cacheLocker.get(Read);
}
}
(5)编写切面类方法
整体切面逻辑:
用户方法调用
│
├─→ @ServiceLock 注解
│
▼
ServiceLockAspect.around()
│
├─→ 1. 解析注解参数(name, keys, lockType, waitTime)
│
├─→ 2. LockInfoHandleFactory.getLockInfoHandle(SERVICE_LOCK)
│ └─→ ServiceLockInfoHandle (继承 AbstractLockInfoHandle)
│ └─→ getLockName(joinPoint, name, keys)
│ ├─→ SpEL 解析 keys 参数 (#orderId → 实际值)
│ └─→ 生成完整锁名: "{prefix}-service_lock:createOrder:123"
│
├─→ 3. ServiceLockFactory.getLock(lockType)
│ └─→ ManageLocker.getReentrantLocker()
│ └─→ 从缓存 Map 返回 RedissonReentrantLocker
│
├─→ 4. ServiceLocker.tryLock(lockName, timeUnit, waitTime)
│ └─→ RedissonReentrantLocker.tryLock()
│ └─→ redissonClient.getLock(lockName).tryLock()
│ └─→ Redis: SET NX EX 命令
│
├─→ 5. 加锁成功?
│ ├─→ YES:
│ │ └─→ joinPoint.proceed() → 执行业务方法
│ │ └─→ finally: ServiceLocker.unlock(lockName)
│ │
│ └─→ NO:
│ ├─→ 检查 customLockTimeoutStrategy
│ │ ├─→ 有自定义策略: 反射调用自定义处理方法
│ │ └─→ 无: lockTimeoutStrategy.handler(lockName)
│ │ └─→ LockTimeOutStrategy.FAIL
│ │ └─→ throw RuntimeException("请求频繁")
│ │
│ └─→ 返回 null 或执行降级逻辑
│
└─→ 返回结果
@Around("@annotation(servicelock)")
public Object around(ProceedingJoinPoint joinPoint, ServiceLock servicelock) throws Throwable {
// 获取锁的名字解析处理器
LockInfoHandle lockInfoHandle = lockInfoHandleFactory.getLockInfoHandle(LockInfoType.SERVICE_LOCK);
// 拼接锁的名字 LOCK:${name}:${key}
String lockName = lockInfoHandle.getLockName(joinPoint, servicelock.name(),servicelock.keys());
// 锁的类型,默认 可重入锁
LockType lockType = servicelock.lockType();
// 尝试加锁失败最多等待时间,默认10s
long waitTime = servicelock.waitTime();
// 时间单位,默认秒
TimeUnit timeUnit = servicelock.timeUnit();
// 获得具体的锁类型
ServiceLocker lock = serviceLockFactory.getLock(lockType);
// 进行加锁
boolean result = lock.tryLock(lockName, timeUnit, waitTime);
// 如果加锁成功
if (result) {
try {
//执行业务逻辑
return joinPoint.proceed();
}finally{
//解锁
lock.unlock(lockName);
}
}else {
log.warn("Timeout while acquiring serviceLock:{}",lockName);
//加锁失败,如果设置了自定义处理,则执行
String customLockTimeoutStrategy = servicelock.customLockTimeoutStrategy();
if (StringUtil.isNotEmpty(customLockTimeoutStrategy)) {
return handleCustomLockTimeoutStrategy(customLockTimeoutStrategy, joinPoint);
}else{
//默认处理
servicelock.lockTimeoutStrategy().handler(lockName);
}
return joinPoint.proceed();
}
}
我们分析上面代码:
首先 LockInfoHandleFactory 是锁信息工厂,返回的有 分布式锁解析处理器 和 幂等解析处理器:
public class LockInfoHandleFactory implements ApplicationContextAware {
private ApplicationContext applicationContext;
public LockInfoHandle getLockInfoHandle(String lockInfoType){
return applicationContext.getBean(lockInfoType,LockInfoHandle.class);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
这里使用 ApplicationContext 用于根据指定的锁类型获取处理器对象:
public interface LockInfoHandle {
/**
* 获取锁信息
* @param joinPoint 切面
* @param name 锁业务名
* @param keys 锁
* @return 锁信息
* */
String getLockName(JoinPoint joinPoint, String name, String[] keys);
/**
* 拼装锁信息
* @param name 锁业务名
* @param keys 锁
* @return 锁信息
* */
String simpleGetLockName(String name,String[] keys);
}
而实现就在我们最开始写的方法:
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.context.expression.MethodBasedEvaluationContext;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.util.ObjectUtils;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
/*
* 锁信息抽象
*/
@Slf4j
public abstract class AbstractLockInfoHandle implements LockInfoHandle {
private static final String LOCK_DISTRIBUTE_ID_NAME_PREFIX = "LOCK_DISTRIBUTE_ID";
private static final String SEPARATOR = ":";
private final ParameterNameDiscoverer nameDiscoverer = new ExtParameterNameDiscoverer();
private final ExpressionParser parser = new SpelExpressionParser();
/**
* 锁信息前缀
* @return 具体前缀
* */
protected abstract String getLockPrefixName();
/**
* 解析出锁的键
* @param joinPoint 切点
* @param name 业务名
* @param keys 参数值
* @return 解析后的锁的键
*
* */
@Override
public String getLockName(JoinPoint joinPoint,String name,String[] keys){
return SpringUtil.getPrefixDistinctionName() + "-" + getLockPrefixName() + SEPARATOR + name + getRelKey(joinPoint, keys);
}
/**
* 解析出锁的键
* @param name 业务名
* @param keys 参数名
* @return 解析后的锁的键
*
* */
@Override
public String simpleGetLockName(String name,String[] keys){
List<String> definitionKeyList = new ArrayList<>();
for (String key : keys) {
if (StringUtil.isNotEmpty(key)) {
definitionKeyList.add(key);
}
}
return SpringUtil.getPrefixDistinctionName() + "-" +
LOCK_DISTRIBUTE_ID_NAME_PREFIX + SEPARATOR + name + SEPARATOR + String.join(SEPARATOR, definitionKeyList);
}
/**
* 获取自定义键
* @param joinPoint 切点
* @param keys 参数名
* @return 获取自定义键
* */
private String getRelKey(JoinPoint joinPoint, String[] keys){
Method method = getMethod(joinPoint);
List<String> definitionKeys = getSpElKey(keys, method, joinPoint.getArgs());
return SEPARATOR + String.join(SEPARATOR, definitionKeys);
}
/**
* 获取自定义键
* @param joinPoint 切点
* @return 切点的方法
* */
private Method getMethod(JoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
if (method.getDeclaringClass().isInterface()) {
try {
method = joinPoint.getTarget().getClass().getDeclaredMethod(signature.getName(),
method.getParameterTypes());
} catch (Exception e) {
log.error("get method error ",e);
}
}
return method;
}
/**
* 获取自定义键
* @param definitionKeys 参数名
* @param method 方法
* @param parameterValues 参数值
* @return 切点的方法
* */
private List<String> getSpElKey(String[] definitionKeys, Method method, Object[] parameterValues) {
List<String> definitionKeyList = new ArrayList<>();
for (String definitionKey : definitionKeys) {
if (!ObjectUtils.isEmpty(definitionKey)) {
EvaluationContext context = new MethodBasedEvaluationContext(null, method, parameterValues, nameDiscoverer);
Object objKey = parser.parseExpression(definitionKey).getValue(context);
definitionKeyList.add(ObjectUtils.nullSafeToString(objKey));
}
}
return definitionKeyList;
}
}
而之前提到过,我们这里主要有 分布式锁解析处理器 和 幂等解析处理器,所以这里就要去实现 getLockPrefixName() 方法:
| 功能类型 | 作用 | 典型业务场景 | 注意事项 |
|---|---|---|---|
| 分布式锁 | 在分布式环境下,保证同一业务操作同时只能有一个线程执行,避免并发冲突 | - 用户注册/登录(同手机号防止重复注册)- 下单 / 支付(防止重复下单、库存超卖)- 绑定第三方账号(如 openid、防止重复绑定)- 多节点定时任务(保证只执行一次)- 数据同步/迁移任务(防止多节点重复处理) | - Key 唯一性必须保证(环境+业务+参数组合)- 锁超时设置合理,防止死锁- 并发量极高时注意 Redis 性能 |
| 幂等/防重复执行 | 防止同一请求或操作被重复执行,保证业务结果只执行一次 | - 表单重复提交(如支付、订单创建)- MQ 消息消费幂等(防止重复投递)- 接口重试 / 重放(保证一次性效果)- 用户操作防抖(按钮防重复点击) | - 幂等 Key 必须唯一(一般用业务ID+环境前缀)- Key 过期策略合理,防止长期占用- 结合锁可提升安全性,防止短时间内重复触发 |
/*
* 锁信息实现(分布式锁)
*/
public class ServiceLockInfoHandle extends AbstractLockInfoHandle {
private static final String LOCK_PREFIX_NAME = "SERVICE_LOCK";
@Override
protected String getLockPrefixName() {
return LOCK_PREFIX_NAME;
}
}
/*
* 锁信息实现(防重复幂等)
*/
public class RepeatExecuteLimitLockInfoHandle extends AbstractLockInfoHandle {
public static final String PREFIX_NAME = "REPEAT_EXECUTE_LIMIT";
@Override
protected String getLockPrefixName() {
return PREFIX_NAME;
}
}
然后继续讲解切面方法:
@Around("@annotation(servicelock)")
public Object around(ProceedingJoinPoint joinPoint, ServiceLock servicelock) throws Throwable {
// 获取锁的名字解析处理器
LockInfoHandle lockInfoHandle = lockInfoHandleFactory.getLockInfoHandle(LockInfoType.SERVICE_LOCK);
// 拼接锁的名字 LOCK:${name}:${key}
String lockName = lockInfoHandle.getLockName(joinPoint, servicelock.name(),servicelock.keys());
// 锁的类型,默认 可重入锁
LockType lockType = servicelock.lockType();
// 尝试加锁失败最多等待时间,默认10s
long waitTime = servicelock.waitTime();
// 时间单位,默认秒
TimeUnit timeUnit = servicelock.timeUnit();
// 获得具体的锁类型
ServiceLocker lock = serviceLockFactory.getLock(lockType);
// 进行加锁
boolean result = lock.tryLock(lockName, timeUnit, waitTime);
// 如果加锁成功
if (result) {
try {
//执行业务逻辑
return joinPoint.proceed();
}finally{
//解锁
lock.unlock(lockName);
}
}else {
log.warn("Timeout while acquiring serviceLock:{}",lockName);
//加锁失败,如果设置了自定义处理,则执行
String customLockTimeoutStrategy = servicelock.customLockTimeoutStrategy();
if (StringUtil.isNotEmpty(customLockTimeoutStrategy)) {
return handleCustomLockTimeoutStrategy(customLockTimeoutStrategy, joinPoint);
}else{
//默认处理
servicelock.lockTimeoutStrategy().handler(lockName);
}
return joinPoint.proceed();
}
}
根据工厂内的 getLockInfoHandle 方法使用 ApplicationContext 类获取 LockInfoHandle 的 Bean 对象,然后该接口定义了 getLockName 方法用于获取锁信息:
public String getLockName(JoinPoint joinPoint,String name,String[] keys){
return SpringUtil.getPrefixDistinctionName() + "-" + getLockPrefixName() + SEPARATOR + name + getRelKey(joinPoint, keys);
}
注:这里面的 getLockPrefixName() 是通过继承 AbstractLockInfoHandle 类 RepeatExecuteLimitLockInfoHandle 与 ServiceLockInfoHandle 两个类实现的 getLockPrefixName 方法(上面提到过)。
于是这样就拿到了锁名,之后通过 @ServiceLock 注解拿到有关锁的一系列参数【这里拿注册手机号的 Service 举例子】:
@ServiceLock(lockType= LockType.Write,name = REGISTER_USER_LOCK,keys = {"#userRegisterDto.mobile"})
public Boolean register(UserRegisterDto userRegisterDto) {}
// 锁的类型,默认 可重入锁
LockType lockType = servicelock.lockType();
// 尝试加锁失败最多等待时间,默认10s
long waitTime = servicelock.waitTime();
// 时间单位,默认秒
TimeUnit timeUnit = servicelock.timeUnit();
上面没有获取到的是有默认值的。随后根据锁的类型从锁工厂中获取真正的锁:
【这里默认的是重入锁 RedissonReentrantLocker】
ServiceLocker lock = serviceLockFactory.getLock(lockType);
@AllArgsConstructor
public class ServiceLockFactory {
// 锁实例存储对象
private final ManageLocker manageLocker;
public ServiceLocker getLock(LockType lockType){
ServiceLocker lock;
switch (lockType) {
case Fair:
lock = manageLocker.getFairLocker();
break;
case Write:
lock = manageLocker.getWriteLocker();
break;
case Read:
lock = manageLocker.getReadLocker();
break;
default:
lock = manageLocker.getReentrantLocker();
break;
}
return lock;
}
}
(6)四种锁实现
下面是四种锁实例:
首先定义一个接口规则:
import org.redisson.api.RLock;
import java.util.concurrent.TimeUnit;
/*
* 分布式锁 方法抽象
*/
public interface ServiceLocker {
/**
* 获取锁
* @param lockKey 锁的key
* @return 结果
* */
RLock getLock(String lockKey);
/**
* 加锁
* @param lockKey 锁的key
* @return 结果
* */
RLock lock(String lockKey);
/**
* 加锁
* @param lockKey 锁的key
* @param leaseTime 释放时间
* @return 结果
* */
RLock lock(String lockKey, long leaseTime);
/**
* 加锁
* @param lockKey 锁的key
* @param unit 时间单位
* @param leaseTime 释放时间
* @return 结果
* */
RLock lock(String lockKey, TimeUnit unit, long leaseTime);
/**
* 加锁
* @param lockKey 锁的key
* @param unit 时间单位
* @param waitTime 等待时间
* @return 结果
* */
boolean tryLock(String lockKey, TimeUnit unit, long waitTime);
/**
* 加锁
* @param lockKey 锁的key
* @param unit 时间单位
* @param waitTime 等待时间
* @param leaseTime 释放时间
* @return 结果
* */
boolean tryLock(String lockKey, TimeUnit unit, long waitTime, long leaseTime);
/**
* 解锁
* @param lockKey 锁的key
* */
void unlock(String lockKey);
/**
* 解锁
* @param lock 锁
* */
void unlock(RLock lock);
}
然后四种锁实现这个接口:
import lombok.AllArgsConstructor;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import java.util.concurrent.TimeUnit;
/*
* 分布式锁 重入锁
*/
@AllArgsConstructor
public class RedissonReentrantLocker implements ServiceLocker {
private final RedissonClient redissonClient;
@Override
public RLock getLock(String lockKey) {
return redissonClient.getLock(lockKey);
}
@Override
public RLock lock(String lockKey) {
RLock lock = redissonClient.getLock(lockKey);
lock.lock();
return lock;
}
@Override
public RLock lock(String lockKey, long leaseTime) {
RLock lock = redissonClient.getLock(lockKey);
lock.lock(leaseTime, TimeUnit.SECONDS);
return lock;
}
@Override
public RLock lock(String lockKey, TimeUnit unit ,long leaseTime) {
RLock lock = redissonClient.getLock(lockKey);
lock.lock(leaseTime, unit);
return lock;
}
@Override
public boolean tryLock(String lockKey, TimeUnit unit, long waitTime) {
RLock lock = redissonClient.getLock(lockKey);
try {
return lock.tryLock(waitTime, unit);
} catch (InterruptedException e) {
return false;
}
}
@Override
public boolean tryLock(String lockKey, TimeUnit unit, long waitTime, long leaseTime) {
RLock lock = redissonClient.getLock(lockKey);
try {
return lock.tryLock(waitTime, leaseTime, unit);
} catch (InterruptedException e) {
return false;
}
}
@Override
public void unlock(String lockKey) {
RLock lock = redissonClient.getLock(lockKey);
lock.unlock();
}
@Override
public void unlock(RLock lock) {
lock.unlock();
}
}
import lombok.AllArgsConstructor;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import java.util.concurrent.TimeUnit;
/*
* 分布式锁 公平锁
*/
@AllArgsConstructor
public class RedissonFairLocker implements ServiceLocker {
private final RedissonClient redissonClient;
@Override
public RLock getLock(String lockKey) {
return redissonClient.getFairLock(lockKey);
}
@Override
public RLock lock(String lockKey) {
RLock lock = redissonClient.getFairLock(lockKey);
lock.lock();
return lock;
}
@Override
public RLock lock(String lockKey, long leaseTime) {
RLock lock = redissonClient.getFairLock(lockKey);
lock.lock(leaseTime, TimeUnit.SECONDS);
return lock;
}
@Override
public RLock lock(String lockKey, TimeUnit unit ,long leaseTime) {
RLock lock = redissonClient.getFairLock(lockKey);
lock.lock(leaseTime, unit);
return lock;
}
@Override
public boolean tryLock(String lockKey, TimeUnit unit, long waitTime) {
RLock lock = redissonClient.getFairLock(lockKey);
try {
return lock.tryLock(waitTime, unit);
} catch (InterruptedException e) {
return false;
}
}
@Override
public boolean tryLock(String lockKey, TimeUnit unit, long waitTime, long leaseTime) {
RLock lock = redissonClient.getFairLock(lockKey);
try {
return lock.tryLock(waitTime, leaseTime, unit);
} catch (InterruptedException e) {
return false;
}
}
@Override
public void unlock(String lockKey) {
RLock lock = redissonClient.getFairLock(lockKey);
lock.unlock();
}
@Override
public void unlock(RLock lock) {
lock.unlock();
}
}
import lombok.AllArgsConstructor;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import java.util.concurrent.TimeUnit;
/*
* 分布式锁 写锁
*/
@AllArgsConstructor
public class RedissonWriteLocker implements ServiceLocker {
private final RedissonClient redissonClient;
@Override
public RLock getLock(String lockKey) {
return redissonClient.getReadWriteLock(lockKey).writeLock();
}
@Override
public RLock lock(String lockKey) {
RLock lock = redissonClient.getReadWriteLock(lockKey).writeLock();
lock.lock();
return lock;
}
@Override
public RLock lock(String lockKey, long leaseTime) {
RLock lock = redissonClient.getReadWriteLock(lockKey).writeLock();
lock.lock(leaseTime, TimeUnit.SECONDS);
return lock;
}
@Override
public RLock lock(String lockKey, TimeUnit unit ,long leaseTime) {
RLock lock = redissonClient.getReadWriteLock(lockKey).writeLock();
lock.lock(leaseTime, unit);
return lock;
}
@Override
public boolean tryLock(String lockKey, TimeUnit unit, long waitTime) {
RLock lock = redissonClient.getReadWriteLock(lockKey).writeLock();
try {
return lock.tryLock(waitTime, unit);
} catch (InterruptedException e) {
return false;
}
}
@Override
public boolean tryLock(String lockKey, TimeUnit unit, long waitTime, long leaseTime) {
RLock lock = redissonClient.getReadWriteLock(lockKey).writeLock();
try {
return lock.tryLock(waitTime, leaseTime, unit);
} catch (InterruptedException e) {
return false;
}
}
@Override
public void unlock(String lockKey) {
RLock lock = redissonClient.getReadWriteLock(lockKey).writeLock();
lock.unlock();
}
@Override
public void unlock(RLock lock) {
lock.unlock();
}
}
import lombok.AllArgsConstructor;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import java.util.concurrent.TimeUnit;
/*
* 分布式锁 读锁
*/
@AllArgsConstructor
public class RedissonReadLocker implements ServiceLocker {
private final RedissonClient redissonClient;
@Override
public RLock getLock(String lockKey) {
return redissonClient.getReadWriteLock(lockKey).readLock();
}
@Override
public RLock lock(String lockKey) {
RLock lock = redissonClient.getReadWriteLock(lockKey).readLock();
lock.lock();
return lock;
}
@Override
public RLock lock(String lockKey, long leaseTime) {
RLock lock = redissonClient.getReadWriteLock(lockKey).readLock();
lock.lock(leaseTime, TimeUnit.SECONDS);
return lock;
}
@Override
public RLock lock(String lockKey, TimeUnit unit ,long leaseTime) {
RLock lock = redissonClient.getReadWriteLock(lockKey).readLock();
lock.lock(leaseTime, unit);
return lock;
}
@Override
public boolean tryLock(String lockKey, TimeUnit unit, long waitTime) {
RLock lock = redissonClient.getReadWriteLock(lockKey).readLock();
try {
return lock.tryLock(waitTime, unit);
} catch (InterruptedException e) {
return false;
}
}
@Override
public boolean tryLock(String lockKey, TimeUnit unit, long waitTime, long leaseTime) {
RLock lock = redissonClient.getReadWriteLock(lockKey).readLock();
try {
return lock.tryLock(waitTime, leaseTime, unit);
} catch (InterruptedException e) {
return false;
}
}
@Override
public void unlock(String lockKey) {
RLock lock = redissonClient.getReadWriteLock(lockKey).readLock();
lock.unlock();
}
@Override
public void unlock(RLock lock) {
lock.unlock();
}
}
(7)加锁以及加锁失败处理
既然刚刚我们已经获取到锁实例对象:
ServiceLocker lock = serviceLockFactory.getLock(lockType);
那么我们就可以加锁了:
boolean result = lock.tryLock(lockName, timeUnit, waitTime);
之后执行完业务逻辑解锁:
if (result) {
try {
return joinPoint.proceed();
}finally{
lock.unlock(lockName);
}
} else {
// 失败策略
log.warn("Timeout while acquiring serviceLock:{}",lockName);
// 加锁失败,如果设置了自定义处理,则执行
String customLockTimeoutStrategy = servicelock.customLockTimeoutStrategy();
if (StringUtil.isNotEmpty(customLockTimeoutStrategy)) {
// 执行自定义处理
return handleCustomLockTimeoutStrategy(customLockTimeoutStrategy, joinPoint);
}else{
// 默认处理 快速失败
servicelock.lockTimeoutStrategy().handler(lockName);
}
return joinPoint.proceed();
}
这里 else 代表获取锁失败,就需要执行 servicelock.lockTimeoutStrategy() 方法【没有获得锁后执行快速失败或者自定义策略】。

- 如果自定义策略处理存在,则执行,否则执行快速失败
- 快速失败的方案为直接抛出异常(快速失败就是下面的 LockTimeOutStrategy)
public enum LockTimeOutStrategy implements LockTimeOutHandler{
/**
* 快速失败
* */
FAIL(){
@Override
public void handler(String lockName) {
String msg = String.format("%s请求频繁",lockName);
throw new RuntimeException(msg);
}
}
}
public interface LockTimeOutHandler {
/**
* 处理
* @param lockName 锁名
* */
void handler(String lockName);
}
而自定义失败:
customLockTimeoutStrategy就是自定义处理的方法名,入参的数量和类型必须和加锁的方法完全一致,然后利用切面时的方法参数,传入到自定义处理的方法中,利用反射的原理来执行自定义的方法。
public Object handleCustomLockTimeoutStrategy(String customLockTimeoutStrategy,JoinPoint joinPoint) {
// prepare invocation context
Method currentMethod = ((MethodSignature) joinPoint.getSignature()).getMethod();
Object target = joinPoint.getTarget();
Method handleMethod = null;
try {
handleMethod = target.getClass().getDeclaredMethod(customLockTimeoutStrategy, currentMethod.getParameterTypes());
handleMethod.setAccessible(true);
} catch (NoSuchMethodException e) {
throw new RuntimeException("Illegal annotation param customLockTimeoutStrategy :" + customLockTimeoutStrategy,e);
}
Object[] args = joinPoint.getArgs();
// invoke
Object result;
try {
result = handleMethod.invoke(target, args);
} catch (IllegalAccessException e) {
throw new RuntimeException("Fail to illegal access custom lock timeout handler: " + customLockTimeoutStrategy ,e);
} catch (InvocationTargetException e) {
throw new RuntimeException("Fail to invoke custom lock timeout handler: " + customLockTimeoutStrategy ,e);
}
return result;
}
方法级别的分布式锁
下面是在上面的基础上,定义了一个工具类:
@AllArgsConstructor
public class ServiceLockTool {
private final LockInfoHandleFactory lockInfoHandleFactory;
private final ServiceLockFactory serviceLockFactory;
/**
* 没有返回值的加锁执行
* @param taskRun 要执行的任务
* @param name 锁的业务名
* @param keys 锁的标识
*
* */
public void execute(TaskRun taskRun,String name,String [] keys) {
execute(taskRun,name,keys,20);
}
/**
* 没有返回值的加锁执行
* @param taskRun 要执行的任务
* @param name 锁的业务名
* @param keys 锁的标识
* @param waitTime 等待时间
*
* */
public void execute(TaskRun taskRun,String name,String [] keys,long waitTime){
execute(LockType.Reentrant,taskRun,name,keys,waitTime);
}
/**
* 没有返回值的加锁执行
* @param lockType 锁类型
* @param taskRun 要执行的任务
* @param name 锁的业务名
* @param keys 锁的标识
*
* */
public void execute(LockType lockType,TaskRun taskRun,String name,String [] keys) {
execute(lockType,taskRun,name,keys,20);
}
/**
* 没有返回值的加锁执行
* @param lockType 锁类型
* @param taskRun 要执行的任务
* @param name 锁的业务名
* @param keys 锁的标识
* @param waitTime 等待时间
*
* */
public void execute(LockType lockType,TaskRun taskRun,String name,String [] keys,long waitTime) {
LockInfoHandle lockInfoHandle = lockInfoHandleFactory.getLockInfoHandle(LockInfoType.SERVICE_LOCK);
String lockName = lockInfoHandle.simpleGetLockName(name,keys);
ServiceLocker lock = serviceLockFactory.getLock(lockType);
boolean result = lock.tryLock(lockName, TimeUnit.SECONDS, waitTime);
if (result) {
try {
taskRun.run();
}finally {
lock.unlock(lockName);
}
}else {
LockTimeOutStrategy.FAIL.handler(lockName);
}
}
/**
* 有返回值的加锁执行
* @param taskCall 要执行的任务
* @param name 锁的业务名
* @param keys 锁的标识
* @return 要执行的任务的返回值
* */
public <T> T submit(TaskCall<T> taskCall,String name,String [] keys){
LockInfoHandle lockInfoHandle = lockInfoHandleFactory.getLockInfoHandle(LockInfoType.SERVICE_LOCK);
String lockName = lockInfoHandle.simpleGetLockName(name,keys);
ServiceLocker lock = serviceLockFactory.getLock(LockType.Reentrant);
boolean result = lock.tryLock(lockName, TimeUnit.SECONDS, 30);
if (result) {
try {
return taskCall.call();
}finally {
lock.unlock(lockName);
}
}else {
LockTimeOutStrategy.FAIL.handler(lockName);
}
return null;
}
/**
* 获得锁
* @param lockType 锁类型
* @param name 锁的业务名
* @param keys 锁的标识
*
* */
public RLock getLock(LockType lockType, String name, String [] keys) {
LockInfoHandle lockInfoHandle = lockInfoHandleFactory.getLockInfoHandle(LockInfoType.SERVICE_LOCK);
String lockName = lockInfoHandle.simpleGetLockName(name,keys);
ServiceLocker lock = serviceLockFactory.getLock(lockType);
return lock.getLock(lockName);
}
/**
* 获得锁
* @param lockType 锁类型
* @param lockName 锁名
*
* */
public RLock getLock(LockType lockType, String lockName) {
ServiceLocker lock = serviceLockFactory.getLock(lockType);
return lock.getLock(lockName);
}
}
那如何使用该工具类加锁呢?
整体流程:
ServiceLockTool.execute(taskRun, name, keys, waitTime)
│
├─→ 1. LockInfoHandleFactory.getLockInfoHandle(SERVICE_LOCK)
│ └─→ simpleGetLockName(name, keys)
│ └─→ 直接拼接(无 SpEL 解析)
│ └─→ "{prefix}-LOCK_DISTRIBUTE_ID:orderCreate:123"
│
├─→ 2. ServiceLockFactory.getLock(lockType)
│ └─→ 同上(获取具体锁实现)
│
├─→ 3. ServiceLocker.tryLock(lockName, SECONDS, waitTime)
│ └─→ 同上(尝试加锁)
│
├─→ 4. 加锁成功?
│ ├─→ YES:
│ │ └─→ try { taskRun.run(); }
│ │ └─→ finally { lock.unlock(lockName); }
│ │
│ └─→ NO:
│ └─→ LockTimeOutStrategy.FAIL.handler(lockName)
│ └─→ throw RuntimeException("请求频繁")
│
└─→ 返回
public void execute(LockType lockType,TaskRun taskRun,String name,String [] keys,long waitTime) {
//获取锁的名字解析处理器
LockInfoHandle lockInfoHandle = lockInfoHandleFactory.getLockInfoHandle(LockInfoType.SERVICE_LOCK);
//拼接锁的名字 LOCK:${name}:${key}
String lockName = lockInfoHandle.simpleGetLockName(name,keys);
//获得具体的锁类型
ServiceLocker lock = serviceLockFactory.getLock(lockType);
//进行加锁
boolean result = lock.tryLock(lockName, TimeUnit.SECONDS, waitTime);
//如果加锁成功
if (result) {
try {
//执行业务逻辑
taskRun.run();
}finally {
//解锁
lock.unlock(lockName);
}
}else {
//默认处理,快速失败
LockTimeOutStrategy.FAIL.handler(lockName);
}
}
这里的 TaskRun 是要进行加锁的逻辑:
@FunctionalInterface
public interface TaskRun {
/**
* 执行任务
* */
void run();
}
而 getLock 方法加锁和 jdk 中的 Lock 加锁方式相同,根据锁的类型和锁的名字以及锁的键值来获得锁的实例,然后用这个锁的实例来进行加锁和解锁的操作:
/**
* 获得锁
* @param lockType 锁类型
* @param name 锁的业务名
* @param keys 锁的标识
*
* */
public RLock getLock(LockType lockType, String name, String [] keys) {
LockInfoHandle lockInfoHandle = lockInfoHandleFactory.getLockInfoHandle(LockInfoType.SERVICE_LOCK);
String lockName = lockInfoHandle.simpleGetLockName(name,keys);
ServiceLocker lock = serviceLockFactory.getLock(lockType);
return lock.getLock(lockName);
}
/**
* 获得锁
* @param lockType 锁类型
* @param lockName 锁名
*
* */
public RLock getLock(LockType lockType, String lockName) {
ServiceLocker lock = serviceLockFactory.getLock(lockType);
return lock.getLock(lockName);
}
加锁方式演示示例:
这种锁是使用命令模式加锁,加锁逻辑包装成TaskRun接口,传入execute方法,类似于线程池的提交:
public void testLock(String name,long id){
serviceLockTool.execute(() -> updateData(name,id),LOCK_DATA, new String[]{String.valueOf(id)});
}
public void updateData(String name,long id){
//模拟修改数据
}
public void testLock(String name,long id){
RLock lock = serviceLockTool.getLock(LockType.Reentrant, LOCK_DATA, new String[]{String.valueOf(id)});
lock.lock();
try {
updateData(name,id);
}finally {
lock.unlock();
}
}
public void updateData(String name,long id){
//模拟修改数据
}
分布式锁实战场景
场景1:防止重复下单
业务需求:同一用户在 5 秒内只能创建一个订单
@Service
public class OrderService {
@ServiceLock(
name = DistributedLockConstants.PROGRAM_ORDER_CREATE_V1,
keys = {"#userId"},
lockType = LockType.Reentrant,
waitTime = 5,
timeUnit = TimeUnit.SECONDS,
lockTimeoutStrategy = LockTimeOutStrategy.FAIL
)
public Long createOrder(Long userId, Long programId, Integer ticketNum) {
// 1. 校验库存
boolean hasStock = programService.checkStock(programId, ticketNum);
if (!hasStock) {
throw new RuntimeException("库存不足");
}
// 2. 扣减库存
programService.deductStock(programId, ticketNum);
// 3. 创建订单
Order order = new Order();
order.setUserId(userId);
order.setProgramId(programId);
order.setTicketNum(ticketNum);
orderDao.insert(order);
return order.getId();
}
}
锁名生成:
- 用户123 →
dev-service_lock:d_program_order_create_v1_lock:123 - 用户456 →
dev-service_lock:d_program_order_create_v1_lock:456
效果:
- 不同用户可以并发下单(锁键不同)
- 同一用户 5 秒内重复请求会被拒绝(锁未释放)
场景2:缓存更新一致性
业务需求:更新商品信息时,删除缓存并防止并发更新
@Service
public class ProductService {
@Autowired
private CacheUtil cacheUtil;
@ServiceLock(
name = DistributedLockConstants.PROGRAM_LOCK,
keys = {"#productId"},
lockType = LockType.Write, // 使用写锁
waitTime = 3
)
@Transactional(rollbackFor = Exception.class)
public void updateProduct(Long productId, ProductDTO dto) {
// 1. 更新数据库
Product product = productDao.getById(productId);
product.setName(dto.getName());
product.setPrice(dto.getPrice());
productDao.update(product);
// 2. 删除缓存(事务提交后执行)
String cacheKey = "product:" + productId;
cacheUtil.delete(cacheKey);
}
@ServiceLock(
name = DistributedLockConstants.GET_PROGRAM_LOCK,
keys = {"#productId"},
lockType = LockType.Read, // 使用读锁
waitTime = 1
)
public Product getProduct(Long productId) {
String cacheKey = "product:" + productId;
// 1. 查缓存
Product product = cacheUtil.get(cacheKey, Product.class);
if (product != null) {
return product;
}
// 2. 查数据库
product = productDao.getById(productId);
// 3. 写缓存
if (product != null) {
cacheUtil.set(cacheKey, product, 300); // 5分钟过期
}
return product;
}
}
读写锁特性:
- 读锁共享 - 多个查询请求可以并发执行
- 写锁独占 - 更新时排斥所有读/写操作
- 写优先 - 避免"写饥饿"问题
场景3:公平抢购(秒杀)
业务需求:秒杀商品时,保证先请求的用户先获得购买机会
@Service
public class SeckillService {
@ServiceLock(
name = "seckillProduct",
keys = {"#productId"},
lockType = LockType.Fair, // 使用公平锁
waitTime = 10,
customLockTimeoutStrategy = "seckillFallback"
)
public boolean seckillProduct(Long userId, Long productId) {
// 1. 检查库存
Integer stock = stockDao.getStock(productId);
if (stock <= 0) {
return false;
}
// 2. 扣减库存(行锁)
int rows = stockDao.deductStock(productId);
if (rows == 0) {
return false;
}
// 3. 创建订单
Order order = new Order();
order.setUserId(userId);
order.setProductId(productId);
orderDao.insert(order);
return true;
}
// 自定义超时处理:加入等待队列
public boolean seckillFallback(Long userId, Long productId) {
// 1. 记录到 Redis 等待队列
redisTemplate.opsForList().rightPush(
"seckill:waiting:" + productId,
userId
);
// 2. 发送 MQ 消息(异步处理)
kafkaTemplate.send("seckill-retry", userId + ":" + productId);
// 3. 返回排队状态
return false; // 前端显示"排队中..."
}
}
场景4:分布式定时任务防重
业务需求:多实例部署时,定时任务只能执行一次
@Component
public class DataSyncTask {
@Autowired
private ServiceLockTool serviceLockTool;
@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点
public void syncData() {
serviceLockTool.execute(
LockType.Reentrant,
() -> {
log.info("开始同步数据...");
// 1. 查询增量数据
List<Data> dataList = dataDao.getIncrementalData();
// 2. 调用第三方接口
for (Data data : dataList) {
thirdPartyApi.sync(data);
}
log.info("数据同步完成,共 {} 条", dataList.size());
},
"dataSyncTask",
new String[]{"daily"},
60 // 最多等待 60 秒
);
}
}
效果:
- 5个实例同时触发定时任务
- 只有1个实例获取锁成功执行
- 其他4个实例获取锁失败,直接返回
幂等性
1.什么是幂等性?
幂等(Idempotent)是一个数学和计算机科学中的概念,主要描述的是一种属性,即一个操作可以被多次应用,但结果仍然保持不变。
在 web 项目中,幂等性同样是一个重要的概念。这主要是因为在网络和分布式系统中,由于网络的不稳定性和其他潜在问题,可能会导致请求被重复发送。如果一个操作不是幂等的,那么重复执行该操作可能会产生不一致的结果或副作用。例如,一个非幂等的操作可能会导致数据被重复添加、更新或删除,从而破坏数据的一致性。
因此,JavaWeb项目需要保证幂等性,主要是为了确保无论请求被发送一次还是多次,系统都能产生相同的结果。这有助于避免由于重复请求导致的数据不一致或其他潜在问题。实现幂等性的方法有很多种,包括但不限于使用数据库唯一索引、乐观锁、分布式锁、令牌等技术
总的来说,幂等性是JavaWeb项目中一个非常重要的概念,它有助于确保系统的稳定性和数据的一致性。通过实现幂等性,我们可以有效地处理重复请求,并减少由于网络不稳定或其他原因导致的潜在问题。
而关于实现幂等的常见方法和技术:
- 唯一标识符(Unique Identifiers):
为每个请求生成一个唯一的标识符(如UUID),并将其作为请求的一部分发送。当接收到请求时,服务器可以检查该标识符是否已处理过。如果已处理,则拒绝或忽略该请求;如果未处理,则处理该请求并记录标识符 - 数据库唯一约束:
使用数据库的唯一约束(如主键或唯一索引)来确保即使多次尝试插入相同的数据,也只有一条记录会被保存。如果尝试插入重复的数据,数据库会抛出异常,服务器可以捕获这个异常并返回幂等性的响应 - 乐观锁(Optimistic Locking):
使用版本号或时间戳来检查数据是否已被其他操作修改过。在更新数据时,如果版本号或时间戳与预期的不符,则拒绝更新并返回冲突信息。这样,即使多次尝试更新相同的数据,也只有一次会成功 - 分布式锁(Distributed Locking):
在分布式系统中,可以使用分布式锁来确保同一时间只有一个节点能够执行某个操作。这可以防止多个节点同时处理相同的请求,从而实现幂等性
2.为什么不直接用分布式锁?
分布式锁的特点是多个请求并发执行,这些请求是来自不同的用户,也就是这些请求虽然要依次等待锁执行,但最终还是要把这些请求都执行完的(执行时间太长超时的异常情况排除),总结起来就是都要获得锁,没有获得锁的请求,也要争取获得锁接着执行。
幂等的特点也是多个请求并发执行,但这些请求是来自同一个用户,也就是说这些请求只要保证第一个请求能执行,其余的请求要直接拒绝掉,总结起来就是只有第一个请求获得锁执行就可以,其余的请求看到已经上了锁,那么就要直接结束掉。
3.设计幂等组件:
首先设计注解:
/*
* 防重复幂等 注解
*/
@Target(value= {ElementType.TYPE, ElementType.METHOD})
@Retention(value= RetentionPolicy.RUNTIME)
public @interface RepeatExecuteLimit {
/**
* 业务名称
* @return name
*/
String name() default "";
/**
* key设置
* @return key
*/
String [] keys();
/**
* 在多长时间内一直保持幂等,如果不配置则以执行方法为准
* */
long durationTime() default 0L;
/**
* 当消息执行已经出发防重复执行的限制时,提示信息
* */
String message() default "提交频繁,请稍后重试";
}
|
属性值 |
类型 |
可否默认 |
含义 |
备注 |
|
name |
String |
Y |
业务名 |
如:consumerApiDataMessage |
|
keys |
String[] |
N |
幂等唯一标识 |
可指定多个,并支持SpEL表达式,如{"#apiData.id"} |
|
durationTime |
long |
Y |
在多长时间内一直保持幂等,如果不配置则以执行方法为准 |
默认0 |
|
message |
String |
Y |
当消息执行已经出发防重复执行的限制时,提示信息 |
默认 提交频繁,请稍后重试 |
之后编写自动装配类:
RepeatExecuteLimitAutoConfiguration是自动装配类,用于加载需要的对象。
repeatExecuteLimitHandle是锁键名处理器、repeatExecuteLimitAspect是幂等切面。
public class RepeatExecuteLimitAutoConfiguration {
@Bean(LockInfoType.REPEAT_EXECUTE_LIMIT)
public LockInfoHandle repeatExecuteLimitHandle(){
return new RepeatExecuteLimitLockInfoHandle();
}
@Bean
public RepeatExecuteLimitAspect repeatExecuteLimitAspect(LocalLockCache localLockCache,
LockInfoHandleFactory lockInfoHandleFactory,
ServiceLockFactory serviceLockFactory,
RedissonDataHandle redissonDataHandle){
return new RepeatExecuteLimitAspect(localLockCache, lockInfoHandleFactory,serviceLockFactory,redissonDataHandle);
}
}
防重复幂等标记:
/*
* 防重复幂等 标记
*/
public class RepeatExecuteLimitConstant {
public static final String PREFIX_NAME = "repeat_flag";
public static final String SUCCESS_FLAG = "success";
}
切面类:
,然后编写切面类:
RepeatExecuteLimitAspect是负责幂等执行的切面也是核心流程,注意这里设置了@Order(-11) ,这么做的原因是因为前面分布式锁也使用了切面的实现方式,而分布式锁的Order是-10,而幂等要在分布式前执行,所以order的值要比分布式锁小。
@Slf4j
@Aspect
@Order(-11)
@AllArgsConstructor
public class RepeatExecuteLimitAspect {
private final LocalLockCache localLockCache;
private final LockInfoHandleFactory lockInfoHandleFactory;
private final ServiceLockFactory serviceLockFactory;
private final RedissonDataHandle redissonDataHandle;
@Around("@annotation(repeatLimit)")
public Object around(ProceedingJoinPoint joinPoint, RepeatExecuteLimit repeatLimit) throws Throwable {
//指定保持幂等的时间
long durationTime = repeatLimit.durationTime();
//提示信息
String message = repeatLimit.message();
Object obj;
//获取锁信息
LockInfoHandle lockInfoHandle = lockInfoHandleFactory.getLockInfoHandle(LockInfoType.REPEAT_EXECUTE_LIMIT);
//解析锁名字
String lockName = lockInfoHandle.getLockName(joinPoint,repeatLimit.name(), repeatLimit.keys());
//幂等标识
String repeatFlagName = PREFIX_NAME + lockName;
//获得幂等标识
String flagObject = redissonDataHandle.get(repeatFlagName);
//如果幂等标识的值为success,说明已经有请求在执行了,这次请求直接结束
if (SUCCESS_FLAG.equals(flagObject)) {
throw new DaMaiFrameException(message);
}
//获取本地锁
ReentrantLock localLock = localLockCache.getLock(lockName,false);
//本地锁获取锁
boolean localLockResult = localLock.tryLock();
//如果上锁失败,说明已经有请求在执行了,这次请求直接结束
if (!localLockResult) {
throw new DaMaiFrameException(message);
}
try {
//获取分布式锁
ServiceLocker lock = serviceLockFactory.getLock(LockType.Reentrant);
//分布式锁获取锁
boolean result = lock.tryLock(lockName, TimeUnit.SECONDS, 0);
//加锁成功执行
if (result) {
try{
//再次获取幂等标识
flagObject = redissonDataHandle.get(repeatFlagName);
//如果幂等标识的值为success,说明已经有请求在执行了,这次请求直接结束
if (SUCCESS_FLAG.equals(flagObject)) {
throw new DaMaiFrameException(message);
}
//执行业务逻辑
obj = joinPoint.proceed();
if (durationTime > 0) {
try {
//业务逻辑执行成功 并且 指定了设置幂等保持时间 设置请求标识
redissonDataHandle.set(repeatFlagName,SUCCESS_FLAG,durationTime,TimeUnit.SECONDS);
}catch (Exception e) {
log.error("getBucket error",e);
}
}
return obj;
} finally {
lock.unlock(lockName);
}
}else{
//获取锁失败,说明已经有请求在执行了,这次请求直接结束
throw new DaMaiFrameException(message);
}
}finally {
localLock.unlock();
}
}
}
其中这里面使用到了获取锁键名处理器:
LockInfoHandle lockInfoHandle = lockInfoHandleFactory.getLockInfoHandle(LockInfoType.REPEAT_EXECUTE_LIMIT);
在前面我们锁有两种实现类型:
public class LockInfoType {
/***
* 防重复执行幂等
*/
public static final String REPEAT_EXECUTE_LIMIT = "repeat_execute_limit";
/***
* 分布式锁
*/
public static final String SERVICE_LOCK = "service_lock";
}
这里因为我们已经装配了幂等锁键名处理器,所以从Spring容器中获取:
public LockInfoHandle getLockInfoHandle(String lockInfoType){
return applicationContext.getBean(lockInfoType,LockInfoHandle.class);
}
@Bean(LockInfoType.REPEAT_EXECUTE_LIMIT)
public LockInfoHandle serviceLockInfoHandle(){
return new RepeatExecuteLimitLockInfoHandle();
}
然后我们来看下RepeatExecuteLimitLockInfoHandle的结构:
它也跟分布式锁处理器一样,继承了 AbstractLockInfoHandle,然后 AbstractLockInfoHandle 继承了 LockInfoHandle:
public class RepeatExecuteLimitLockInfoHandle extends AbstractLockInfoHandle {
public static final String PREFIX_NAME = "REPEAT_EXECUTE_LIMIT";
@Override
protected String getLockPrefixName() {
return PREFIX_NAME;
}
}
AbstractLockInfoHandle 是解析锁信息的核心处理类,使用 Spel 进行解析就是在此类进行执行的,之所以将逻辑抽取到 AbstractLockInfoHandle 的原因是除了幂等组件需要处理锁信息外,分布式锁组件也需要此功能。
/*
* 锁信息抽象
*/
@Slf4j
public abstract class AbstractLockInfoHandle implements LockInfoHandle {
private static final String LOCK_DISTRIBUTE_ID_NAME_PREFIX = "LOCK_DISTRIBUTE_ID";
private static final String SEPARATOR = ":";
private final ParameterNameDiscoverer nameDiscoverer = new ExtParameterNameDiscoverer();
private final ExpressionParser parser = new SpelExpressionParser();
/**
* 锁信息前缀
* @return 具体前缀
* */
protected abstract String getLockPrefixName();
@Override
public String getLockName(JoinPoint joinPoint,String name,String[] keys){
return SpringUtil.getPrefixDistinctionName() + "-" + getLockPrefixName() + SEPARATOR + name + getRelKey(joinPoint, keys);
}
@Override
public String simpleGetLockName(String name,String[] keys){
List<String> definitionKeyList = new ArrayList<>();
for (String key : keys) {
if (StringUtil.isNotEmpty(key)) {
definitionKeyList.add(key);
}
}
return SpringUtil.getPrefixDistinctionName() + "-" +
LOCK_DISTRIBUTE_ID_NAME_PREFIX + SEPARATOR + name + SEPARATOR + String.join(SEPARATOR, definitionKeyList);
}
/**
* 获取自定义键
* */
private String getRelKey(JoinPoint joinPoint, String[] keys){
Method method = getMethod(joinPoint);
List<String> definitionKeys = getSpElKey(keys, method, joinPoint.getArgs());
return SEPARATOR + String.join(SEPARATOR, definitionKeys);
}
private Method getMethod(JoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
if (method.getDeclaringClass().isInterface()) {
try {
method = joinPoint.getTarget().getClass().getDeclaredMethod(signature.getName(),
method.getParameterTypes());
} catch (Exception e) {
log.error("get method error ",e);
}
}
return method;
}
private List<String> getSpElKey(String[] definitionKeys, Method method, Object[] parameterValues) {
List<String> definitionKeyList = new ArrayList<>();
for (String definitionKey : definitionKeys) {
if (!ObjectUtils.isEmpty(definitionKey)) {
EvaluationContext context = new MethodBasedEvaluationContext(null, method, parameterValues, nameDiscoverer);
Object objKey = parser.parseExpression(definitionKey).getValue(context);
definitionKeyList.add(ObjectUtils.nullSafeToString(objKey));
}
}
return definitionKeyList;
}
}
public interface LockInfoHandle {
/**
* 获取锁信息
* @param joinPoint 切面
* @param name 锁业务名
* @param keys 锁
* @return 锁信息
* */
String getLockName(JoinPoint joinPoint, String name, String[] keys);
/**
* 拼装锁信息
* @param name 锁业务名
* @param keys 锁
* @return 锁信息
* */
String simpleGetLockName(String name,String[] keys);
}
之后我们继续分析切面类:

下面的代码用于将获取的锁键名 lockName 再拼接前缀,原因是将幂等的锁的键名区分开,获得标识 flagObject 后,如果 flagObject = success,说明上一个请求已经执行完毕设置了幂等标识,那么这次请求直接结束掉,如果没有标识,那么继续执行请求:
public static final String PREFIX_NAME = "repeat_flag";
String repeatFlagName = PREFIX_NAME + lockName;
String flagObject = redissonDataHandle.get(repeatFlagName);
if (SUCCESS_FLAG.equals(flagObject)) {
throw new DaMaiFrameException(message);
}
这里的Redission操作类封装:
@AllArgsConstructor
public class RedissonDataHandle {
private final RedissonClient redissonClient;
public String get(String key){
return (String)redissonClient.getBucket(key).get();
}
public void set(String key,String value){
redissonClient.getBucket(key).set(value);
}
public void set(String key,String value,long timeToLive, TimeUnit timeUnit){
redissonClient.getBucket(key).set(value,getDuration(timeToLive,timeUnit));
}
public Duration getDuration(long timeToLive, TimeUnit timeUnit){
switch (timeUnit) {
case MINUTES -> {
return Duration.ofMinutes(timeToLive);
}
case HOURS -> {
return Duration.ofHours(timeToLive);
}
case DAYS -> {
return Duration.ofDays(timeToLive);
}
default -> {
return Duration.ofSeconds(timeToLive);
}
}
}
}
本地锁:
然后到了本地锁:

ReentrantLock localLock = localLockCache.getLock(lockName,true);
boolean localLockResult = localLock.tryLock();
if (!localLockResult) {
throw new DaMaiFrameException(message);
}
这里 localLockCache 是本地锁缓存,可根据锁名和锁类型(公平锁/非公平锁)来获得ReentrantLock的实例:
LocalLockCache其实是用 Caffeine 缓存来保存的锁信息,并可以设置锁实例的保存时间,默认是2小时,这个时间可以根据 durationTime 来进行配置,如果时间过大,那么锁的实例就会过多,对项目的内存就会有压力。如果时间过小,那么构建锁的频率就会增加,性能就会受到影响,使用时,可根据业务特点进行灵活配置。
有关Caffeine可查看下面博客:
public class LocalLockCache {
/**
* 本地锁缓存
* */
private Cache<String, ReentrantLock> localLockCache;
/**
* 本地锁的过期时间(小时单位)
* */
@Value("${durationTime:48}")
private Integer durationTime;
@PostConstruct
public void localLockCacheInit(){
localLockCache = Caffeine.newBuilder()
.expireAfterWrite(durationTime, TimeUnit.HOURS)
.build();
}
/**
* 获得锁,Caffeine的get是线程安全的
* */
public ReentrantLock getLock(String lockKey,boolean fair){
return localLockCache.get(lockKey, key -> new ReentrantLock(fair));
}
}
公平锁与非公平锁:
公平锁和非公平锁是在并发编程中常用的两种锁的类型,它们在资源的访问方式上有所不同,影响着程序的公平性和效率。
公平锁:
- 公平锁是指多个线程按照申请锁的顺序来获取锁,先来先得,FIFO(先进先出)原则。即当线程尝试获取锁时,如果发现锁已被其他线程占用,则该线程会进入等待队列,等待其他线程释放锁。
- 公平锁保证了所有线程都有公平竞争获取锁的机会,不存在线程饥饿(某些线程一直无法获取锁)的情况。
非公平锁:
- 非公平锁没有先来先得的规则,线程在尝试获取锁时,如果发现锁已经被其他线程占用,它会采取一些手段(如自旋等待)来尝试获取锁,而不是直接进入等待队列。
- 当持有锁的线程释放锁时,会选择一个等待线程来获取锁,这个选择可能不是按照先来先得的原则,因此可能会导致某些线程长时间等待,降低了公平性。
效率方面:
- 公平锁的效率可能会比非公平锁低一些,因为公平锁需要维护一个等待队列,线程进入队列和唤醒队列中的线程需要进行上下文切换,这会带来一定的性能开销。
- 非公平锁在尝试获取锁时会尽可能地避免线程的上下文切换,因为它可能会通过自旋等待来获取锁,而不是直接进入等待队列,所以在一些情况下,非公平锁的效率可能会略高于公平锁。
场景选择:
选择公平锁还是非公平锁取决于具体的场景和需求。如果程序对公平性要求较高,希望所有线程都能有公平竞争获取锁的机会,那么可以选择公平锁;如果程序对性能要求较高,可以容忍部分线程长时间等待,那么可以选择非公平锁。如果想追求绝对的性能,那就使用非公平锁。如果想追求用户体验想先请求的用户先获得锁,那就使用公平锁。
锁的粒度:
当获得本地锁实例后,会去尝试加锁,如果加锁失败,说明之前已经有请求获得了锁在执行中没有释放掉,那么这次请求直接结束:

为什么这里要先加入本地锁在加分布式锁?
之所以先使用本地锁去加锁的原因是,可以很大程度上节省分布式锁的资源,虽然分布式锁是利用 redis 实现的,redis 的性能又非常的高,但是它再高依旧存在网络损耗,而本地锁的操作都是基于内存中,一个是内存中操作,一个是网络操作,前者的效率可是后者的几十倍差距。
如果一秒内有100个请求,服务的实例有5个,那么每个实例就有20个请求,这20个请求就可以靠本地锁来拦截掉,那么到分布式锁那里,就有5个请求来获得锁了,其余的95个请求都可以被提前结束掉。
相当于本地锁在 JVM 内只允许 1 个线程进入,拦住大量请求,然后分布式锁跨 JVM / 跨节点互斥,用来保证全局只有一个请求执行。
这是一个经典的思想,优先考虑本地内存操作,经过本地内存操作后,再去操作第三方中间件。
分布式锁:
当本地锁获得了锁之后,还要用分布式锁去尝试获得锁,因为本地锁只能保证当前自己的实例范围内能锁住请求,微服务多个实例部署的话,就需要分布式锁了。

当通过分布式锁工厂获取到客户端实例后,就会尝试去获取分布式锁了,如果加锁失败,说明之前已经有请求获得了锁在执行中没有释放掉,那么这次请求直接结束。
如果加锁成功则执行业务逻辑joinPoint.proceed()
- 如果执行业务逻辑成功,如果设置了幂等保持时间,那么设置幂等标识
- 如果执行业务逻辑失败,那么直接释放锁
这里获得分布式锁的方法如下:
/*
* 分布式锁类型工厂
*/
@AllArgsConstructor
public class ServiceLockFactory {
private final ManageLocker manageLocker;
public ServiceLocker getLock(LockType lockType){
ServiceLocker lock;
switch (lockType) {
case Fair:
lock = manageLocker.getFairLocker();
break;
case Write:
lock = manageLocker.getWriteLocker();
break;
case Read:
lock = manageLocker.getReadLocker();
break;
default:
lock = manageLocker.getReentrantLocker();
break;
}
return lock;
}
}
import org.redisson.api.RLock;
import java.util.concurrent.TimeUnit;
/*
* 分布式锁 方法抽象
*/
public interface ServiceLocker {
/**
* 获取锁
* @param lockKey 锁的key
* @return 结果
* */
RLock getLock(String lockKey);
/**
* 加锁
* @param lockKey 锁的key
* @return 结果
* */
RLock lock(String lockKey);
/**
* 加锁
* @param lockKey 锁的key
* @param leaseTime 释放时间
* @return 结果
* */
RLock lock(String lockKey, long leaseTime);
/**
* 加锁
* @param lockKey 锁的key
* @param unit 时间单位
* @param leaseTime 释放时间
* @return 结果
* */
RLock lock(String lockKey, TimeUnit unit, long leaseTime);
/**
* 加锁
* @param lockKey 锁的key
* @param unit 时间单位
* @param waitTime 等待时间
* @return 结果
* */
boolean tryLock(String lockKey, TimeUnit unit, long waitTime);
/**
* 加锁
* @param lockKey 锁的key
* @param unit 时间单位
* @param waitTime 等待时间
* @param leaseTime 释放时间
* @return 结果
* */
boolean tryLock(String lockKey, TimeUnit unit, long waitTime, long leaseTime);
/**
* 解锁
* @param lockKey 锁的key
* */
void unlock(String lockKey);
/**
* 解锁
* @param lock 锁
* */
void unlock(RLock lock);
}
然后四种锁实现这个接口,这个在讲解分布式锁已经提到过了。
然后获取到分布式锁后内部:

这里使用方法再次获取幂等标识:
flagObject = redissonDataHandle.get(repeatFlagName);
这是 第二次 查 Redis 幂等标识。为什么必须再查一次?
因为可能存在这种时间窗口:
线程 A:通过第一次校验
线程 B:在 A 之前执行成功并设置 SUCCESS_FLAG
线程 A:此时已经拿到了锁
锁 ≠ 业务已经执行过。
如果已经成功过,直接拦截:
if (SUCCESS_FLAG.equals(flagObject)) {
throw new DaMaiFrameException(message);
}
当执行 obj = joinPoint.proceed(); 业务成功后,设置幂等成功标识:
if (durationTime > 0) {
try {
redissonDataHandle.set(
repeatFlagName,
SUCCESS_FLAG,
durationTime,
TimeUnit.SECONDS
);
} catch (Exception e) {
log.error("getBucket error", e);
}
}
如何使用注解标记:
@RepeatExecuteLimit(name = RepeatExecuteLimitConstants.CONSUMER_API_DATA_MESSAGE,keys = {"#apiData.id"})
public void saveApiData(ApiData apiData){
ApiData dbApiData = apiDataMapper.selectById(apiData.getId());
if (Objects.isNull(dbApiData)) {
log.info("saveApiData apiData:{}", JSON.toJSONString(apiData));
apiDataMapper.insert(apiData);
}
}
/*
* 防重复幂等业务名管理
**/
public class RepeatExecuteLimitConstants {
public static final String CONSUMER_API_DATA_MESSAGE = "consumer_api_data_message";
public static final String CREATE_PROGRAM_ORDER = "create_program_order";
public final static String CANCEL_PROGRAM_ORDER = "cancel_program_order";
public static final String CREATE_PROGRAM_ORDER_MQ = "create_program_order_mq";
public static final String PROGRAM_CACHE_REVERSE_MQ = "program_cache_reverse_mq";
}
更多推荐
所有评论(0)