目录

Redission分布式锁原理

for update

Redis锁

Redission

使用 AOP 实现分布式锁

1.根据环境获取环境名:

2.获取锁名的方法:

3.使用 AOP 实现分布式锁调度:

(1)事务切面 与 锁切面 的先后执行顺序导致的问题

(2)设计一个 @ServiceLock 注解

(3)编写自动配置类

(4)锁管理缓存对象实例

(5)编写切面类方法

(6)四种锁实现

(7)加锁以及加锁失败处理

方法级别的分布式锁

分布式锁实战场景

场景1:防止重复下单

场景2:缓存更新一致性

场景3:公平抢购(秒杀)

场景4:分布式定时任务防重

幂等性

1.什么是幂等性?

2.为什么不直接用分布式锁?

3.设计幂等组件:

切面类:

本地锁:

公平锁与非公平锁:

公平锁:

非公平锁:

效率方面:

场景选择:

锁的粒度:

为什么这里要先加入本地锁在加分布式锁?

分布式锁:

如何使用注解标记:


分布式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 会对此记录加上行级锁,在超时,提交,回滚会进行释放。

缺点:

  1. 当请求等待锁释放时,不能灵活的控制加锁时间、等待锁的时间
  2. 如果在一个事务中,开始的时候就使用for update的话,则需要这个事务执行完提交或回滚才能够解锁,不能很好的控制锁的粒度,并发性会降低。
  3. 在Repeatable Read的隔离级别下有可能会产生死锁。https://www.cnblogs.com/micrari/p/8029710.htmlhttps://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 实用工具包中常用接口,为使用者提供了 一系列具有分布式特性的常用工具类。

  1. 指定一个 key 作为锁标记,存入 Redis 中,指定一个 唯一的用户标识 作为 value。
  2. 当 key 不存在时才能设置值,确保同一时间只有一个客户端进程获得锁,满足 互斥性 特性。
  3. 设置一个过期时间,防止因系统异常导致没能删除这个 key,满足 防死锁 特性。
  4. 当处理完业务之后需要清除这个 key 来释放锁,清除 key 时需要校验 value 值,需要满足 只有加锁的人才能释放锁
  5. WatchDog 机制 能够很好的解决锁续期的问题,预防死锁
  6. 能够灵活的设置加锁时间,等待锁时间,释放锁失败后锁的存在时间。

现在来说,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 {
    //省略
}

总结:

对于 事务和业务切面的执行顺序问题 导致了锁的范围没有将整个事务包裹住,我们可以通过两种方法解决:

  1. 将锁的切面放在 controller 的方法上,这样锁的切面肯定会先于事务切面执行
  2. 如果锁的切面和事务切面在一个方法上,那么指定锁切面的 order 值,比事务切面 order 值小即可(事务切面order默认为Integer.MAX_VALUE)

而对于锁的方法级使用:

lock.lock(lockName);
try {
    //执行逻辑
}finally {
    lock.unLock(lockName);
}

一般这都是在service层进行加锁的,所以出现的问题和上述切面问题相同,都是发生了事务切面在锁的切面之前执行,导致锁没有把事务包裹住。所以使用这种方法级别的分布式锁,要考虑在 Controller 控制层加锁或者设计出加锁层,在控制层和service层中间,当加锁后,再调用service的方法。

(2)设计一个 @ServiceLock 注解

注解 @ServiceLock 加在 Service 层的方法上即可,锁的键可以根据入参的多个参数一起配置,这样比较灵活,如果使用用户 Token 的话锁的粒度太大。而对于锁的参数主要有以下:

  1. 锁的类型包含可重入锁(默认)公平锁读锁写锁
  2. 可配置锁的等待时间
  3. 提供了加锁失败后直接拒绝(默认)和自定义处理的两种处理方式

首先编写一个枚举类来列出锁的四种类型:

/*
 * 分布式锁 锁类型
 */
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项目中一个非常重要的概念,它有助于确保系统的稳定性和数据的一致性。通过实现幂等性,我们可以有效地处理重复请求,并减少由于网络不稳定或其他原因导致的潜在问题。

而关于实现幂等的常见方法和技术:

  1. 唯一标识符(Unique Identifiers)
    为每个请求生成一个唯一的标识符(如UUID),并将其作为请求的一部分发送。当接收到请求时,服务器可以检查该标识符是否已处理过。如果已处理,则拒绝或忽略该请求;如果未处理,则处理该请求并记录标识符
  2. 数据库唯一约束
    使用数据库的唯一约束(如主键或唯一索引)来确保即使多次尝试插入相同的数据,也只有一条记录会被保存。如果尝试插入重复的数据,数据库会抛出异常,服务器可以捕获这个异常并返回幂等性的响应
  3. 乐观锁(Optimistic Locking)
    使用版本号或时间戳来检查数据是否已被其他操作修改过。在更新数据时,如果版本号或时间戳与预期的不符,则拒绝更新并返回冲突信息。这样,即使多次尝试更新相同的数据,也只有一次会成功
  4. 分布式锁(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可查看下面博客:

高并发处理 --- Caffeine内存缓存库 + 使用Redis Stream解决缓存一致性https://blog.csdn.net/2302_79840586/article/details/144702499?spm=1001.2014.3001.5501

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));
    }
}
公平锁与非公平锁:

公平锁和非公平锁是在并发编程中常用的两种锁的类型,它们在资源的访问方式上有所不同,影响着程序的公平性和效率。

公平锁
  1. 公平锁是指多个线程按照申请锁的顺序来获取锁,先来先得,FIFO(先进先出)原则。即当线程尝试获取锁时,如果发现锁已被其他线程占用,则该线程会进入等待队列,等待其他线程释放锁。
  2. 公平锁保证了所有线程都有公平竞争获取锁的机会,不存在线程饥饿(某些线程一直无法获取锁)的情况。
非公平锁
  1. 非公平锁没有先来先得的规则,线程在尝试获取锁时,如果发现锁已经被其他线程占用,它会采取一些手段(如自旋等待)来尝试获取锁,而不是直接进入等待队列。
  2. 当持有锁的线程释放锁时,会选择一个等待线程来获取锁,这个选择可能不是按照先来先得的原则,因此可能会导致某些线程长时间等待,降低了公平性。
效率方面:
  • 公平锁的效率可能会比非公平锁低一些,因为公平锁需要维护一个等待队列,线程进入队列和唤醒队列中的线程需要进行上下文切换,这会带来一定的性能开销。
  • 非公平锁在尝试获取锁时会尽可能地避免线程的上下文切换,因为它可能会通过自旋等待来获取锁,而不是直接进入等待队列,所以在一些情况下,非公平锁的效率可能会略高于公平锁。
场景选择:

选择公平锁还是非公平锁取决于具体的场景和需求。如果程序对公平性要求较高,希望所有线程都能有公平竞争获取锁的机会,那么可以选择公平锁;如果程序对性能要求较高,可以容忍部分线程长时间等待,那么可以选择非公平锁。如果想追求绝对的性能,那就使用非公平锁。如果想追求用户体验想先请求的用户先获得锁,那就使用公平锁。

锁的粒度:

当获得本地锁实例后,会去尝试加锁,如果加锁失败,说明之前已经有请求获得了锁在执行中没有释放掉,那么这次请求直接结束:

为什么这里要先加入本地锁在加分布式锁?

之所以先使用本地锁去加锁的原因是,可以很大程度上节省分布式锁的资源,虽然分布式锁是利用 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";
}

Logo

更多推荐