Integration在基于Spring的应用程序中实现轻量级消息传递,并支持通过声明适配器与外部系统集成。 Spring Integration的主要目标是提供一个简单的模型来构建企业集成解决方案,同时保持关注点的分离,这对于生成可维护,可测试的代码至关重要。我们熟知的 Spring Cloud Stream的底层就是Spring Integration。
Spring Integration提供的全局锁目前为如下存储提供了实现:
- Gemfire
- JDBC
- Redis
- Zookeeper
它们使用相同的API抽象,这意味着,不论使用哪种存储,你的编码体验是一样的。试想一下你目前是基于zookeeper实现的分布式锁,哪天你想换成redis的实现,我们只需要修改相关依赖和配置就可以了,无需修改代码。下面是你使用 Spring Integration 实现分布式锁时需要关注的方法:
所以本文章旨在搭建基础的全局锁Integration,后面的分布式锁可以基于此实现,让代码更加优雅解耦
全局锁与分布式锁的区别
- 全局锁(Global Lock): 全局锁作用于单个进程或单个实例,控制了整个进程或实例中的资源访问。在多线程应用程序中,全局锁可以确保在同一进程中不同线程之间的并发访问互斥。
- 分布式锁(Distributed Lock): 分布式锁用于跨多个进程或多个节点的分布式系统中,用于控制不同进程或节点之间的并发访问。它可以确保在整个分布式系统中的不同节点之间的互斥访问。
本章分为两部分进行(声明式与自定义注解)
首先定义依赖
声明式
声明式的可以直接用,我创建了一个多线程任务去执行这个加锁解锁逻辑如下
加锁后打印一句话然后再解锁,那怎么判断这样是正确的?每一个线程加锁和解锁必定在一起,因为是同一把锁,第二个线程只要第一个线程解锁后才能拿到锁
- obtain():根据名字获取锁
- tryLock()尝试加锁
配置一个本地锁
@SpringBootConfiguration
@Slf4j
public class LocalLockConfig {
/**
* 配置本地锁z
* @return
*/
@Bean
public LockRegistry localLockRegistry(){
LockRegistry lockRegistry = new DefaultLockRegistry();
log.info("the local lock is loaded successfully!");
return lockRegistry;
}
}
测试
/**
* 测试手动获取锁
*/
@Test
public void lockRegistryTest() throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(10);
for(int i = 0; i < 10; i++){
threadPoolTaskExecutor.execute(()->{
Lock lock = lockRegistry.obtain(LockConstants.R_PAN_LOCK);
boolean lockResult = false;
try {
lockResult = lock.tryLock(60L, TimeUnit.SECONDS);
if(lockResult) {
System.out.println(Thread.currentThread().getName() + " get the lock");
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
if(lockResult) {
System.out.println(Thread.currentThread().getName() + " release the lock");
lock.unlock();
}
}
countDownLatch.countDown();
});
}
countDownLatch.await();
}
打印如下说明没问题
自定义注解
定义一个核心模块,架构如下
首先是定义一个注解类
- 定义锁的名称
- 锁过期时长
- 自定义锁的key
- 锁的生成器
-
@Documented(当一个注解被标记时,如果你使用该注解标记了一个类、方法或字段,那么在生成 Java 文档时,这个注解的信息将会包含在文档中,方便其他开发者查阅。) @Retention(RetentionPolicy.RUNTIME)(表示作用于运行) @Target({ElementType.METHOD})(表示作用于方法上面)
/**
* 自定义锁的注解
* @author yubin
* @create 2024-04-16-14:25
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Lock {
/**
* 锁的名称
* @return
*/
String name() default "";
/**
* 锁的过期时长
* @return
*/
long expireSecond() default 60L;
/**
* 自定义锁的key,支持el表达式
* @return
*/
String[] keys() default {};
/**
* 制定锁的生成器
* @return
*/
Class<? extends KeyGenerator> keyGenerator() default StandardKeyGenerator.class;
}
定义了一些参数如下
public interface LockConstants {
/**
* 公用lock的名称
*/
String R_PAN_LOCK = "r-pan-lock";
/**
* 公用lock的path
* 主要针对zk等节点型的软件
*/
String R_PAN_LOCK_PATH = "/r-pan-lock";
}
接着定义一个类封装锁的上下文信息(里面参数都是跟方法类有关系的)
-
init(ProceedingJoinPoint proceedingJoinPoint)
方法是一个静态工厂方法,用于初始化LockContext
对象。它接收一个ProceedingJoinPoint
对象作为参数,该对象包含了切点方法的执行上下文信息,并返回一个初始化好的LockContext
对象。 -
doInit(LockContext lockContext, ProceedingJoinPoint proceedingJoinPoint)
方法是一个私有静态方法,用于执行LockContext
对象的初始化过程。它接收一个LockContext
对象和一个ProceedingJoinPoint
对象作为参数,将切点方法的相关信息解析并设置到LockContext
对象的属性中。 -
ProceedingJoinPoint
是 Spring AOP 中的一个接口,它代表了正在执行的连接点(Join Point),并提供了许多方法来获取连接点的相关信息。简单来说就是要被增强的方法,可以获取他的信息
@Data
public class LockContext {
/**
* 切面方法所属类的名称
*/
private String className;
/**
* 切点方法的名称
*/
private String methodName;
/**
* 切点方法上面标准的自定义锁注解
*/
private Lock annotation;
/**
* 类的class对象
*/
private Class classType;
/**
* 当前调用的方法的实体
*/
private Method method;
/**
* 参数列表实体
*/
private Object[] args;
/**
* 参数列表类型
*/
private Class[] parameterTypes;
/**
* 代理对象实体
*/
private Object target;
/**
* 初始化实体对象
* @param proceedingJoinPoint
* @return
*/
public static LockContext init(ProceedingJoinPoint proceedingJoinPoint){
LockContext lockContext = new LockContext();
doInit(lockContext,proceedingJoinPoint);
return lockContext;
}
private static void doInit(LockContext lockContext, ProceedingJoinPoint proceedingJoinPoint) {
Signature signature = proceedingJoinPoint.getSignature();
Object[] args = proceedingJoinPoint.getArgs();
Object target = proceedingJoinPoint.getTarget();
String methodName = signature.getName();
Class classType = signature.getDeclaringType();
String className = signature.getDeclaringTypeName();
Class[] parameterTypes = ((MethodSignature) signature).getParameterTypes();
Method method = ((MethodSignature) signature).getMethod();
Lock annotation = method.getAnnotation(Lock.class);
lockContext.setArgs(args);
lockContext.setTarget(target);
lockContext.setMethodName(methodName);
lockContext.setClassType(classType);
lockContext.setClassName(className);
lockContext.setParameterTypes(parameterTypes);
lockContext.setMethod(method);
lockContext.setAnnotation(annotation);
}
}
接着前面的锁提到了需要锁的生成器,接下我我们定义锁的生成器
这种一般都是需要写接口->抽象类->实体类来进行
首先是接口,定义了一个方法返回string的字符串
public interface KeyGenerator {
/**
* 生成锁的key
* @param lockContext
* @return
*/
String generateKey(LockContext lockContext);
}
接着定义抽象类,这是key生成器的公用父类,里面可以实现公用的方法
通过这个抽象类,可以方便地定义和实现不同类型的锁键生成器,只需要继承 AbstractKeyGenerator
类,并实现 doGenerateKey()
方法,即可定制化地生成符合业务需求的锁键值。
generateKey(LockContext lockContext)
方法是实现了KeyGenerator
接口中定义的生成锁键的方法。该方法接收一个LockContext
对象作为参数,其中包含了切面方法的上下文信息,例如方法名、类名、方法参数等。然后根据LockContext
中的信息,生成锁的键值。-
在
generateKey()
方法中,首先从lockContext
中获取切点方法上的Lock
注解,然后获取该注解中定义的键(keys)数组。 -
接着创建了一个空的
KeyValueMap
,用于保存键值对。然后遍历keys
数组,利用 SpEL 表达式工具类(SpElUtil
)根据注解中定义的键,从lockContext
中获取对应的值,并将键值对放入KeyValueMap
中。 -
KeyValueMap
存储了 SpEL 表达式中键值对的 HashMap,其中键是 SpEL 表达式中定义的键,而值是根据 SpEL 表达式求值得到的结果。 -
最后调用了
doGenerateKey()
方法,该方法是一个抽象方法,需要子类实现具体的键生成逻辑。它接收lockContext
对象和KeyValueMap
参数,并返回最终生成的锁键值。
public abstract class AbstractKeyGenerator implements KeyGenerator{
@Override
public String generateKey(LockContext lockContext) {
Lock annotation = lockContext.getAnnotation();
String[] keys = annotation.keys();
Map<String,String> KeyValueMap = Maps.newHashMap();
if(ArrayUtils.isNotEmpty(keys)){
Arrays.stream(keys).forEach(key->{
KeyValueMap.put(key, SpElUtil.getStringValue(key,lockContext.getClassName(),lockContext.getMethodName(),lockContext.getClassType(),
lockContext.getMethod(),lockContext.getArgs(),lockContext.getParameterTypes(),lockContext.getTarget()));
});
}
return doGenerateKey(lockContext,KeyValueMap);
}
/**
* 具体逻辑下层到子类实现
* @param lockContext
* @param keyValueMap
* @return
*/
protected abstract String doGenerateKey(LockContext lockContext, Map<String, String> keyValueMap);
}
接着介绍一个工具类,可以解析SpEL表达式的,让注解可以支持SpEL表达式
这个工具类 SpElUtil
主要用于解析 SpEL(Spring Expression Language)表达式,并根据表达式获取相应的值。让我来逐步解释这个类的功能和结构:
-
SpElUtil
类提供了三个静态方法,用于解析 SpEL 表达式并获取对应的值:getCustomerValue()
: 解析 SpEL 表达式,并将结果转换为指定类型。getValue()
: 解析 SpEL 表达式,获取结果但不进行类型转换。getStringValue()
: 解析 SpEL 表达式,并将结果转换为字符串类型。
-
在这些方法中,都需要传入以下参数:
spElExpression
: 要解析的 SpEL 表达式。className
: 类的名称。methodName
: 方法的名称。classType
: 类的Class
对象。method
: 方法的Method
对象。args
: 方法的参数。parameterTypes
: 方法的参数类型。target
: 目标对象。
-
这个类内部使用了
RPanExpressionEvaluator
内部类来实现 SpEL 表达式的解析和缓存。RPanExpressionEvaluator
继承了CachedExpressionEvaluator
类,用于缓存解析过的表达式,提高解析效率。 -
RPanExpressionRootObject
是一个私有静态内部类,用于作为表达式的根对象,提供了一些常见的表达式格式的支持,例如#root.className
、#root.methodName
等。 -
在
RPanExpressionEvaluator
内部类中,使用了ConcurrentHashMap
来缓存解析过的表达式和目标方法,以提高解析效率。
总的来说,这个工具类提供了一组方法,用于方便地解析 SpEL 表达式,并根据表达式获取相应的值,可用于动态地获取对象的属性、方法参数等信息
/**
* 解析SpEl表达式解析器工具类
*/
public class SpElUtil {
private static final RPanExpressionEvaluator expressionEvaluator = new RPanExpressionEvaluator();
/**
* 解析SpEl表达式
*
* @param spElExpression
* @param returnType
* @param className
* @param methodName
* @param classType
* @param method
* @param args
* @param parameterTypes
* @param target
* @param <T>
* @return
*/
public static <T> T getCustomerValue(String spElExpression,
Class<T> returnType,
String className,
String methodName,
Class classType,
Method method,
Object[] args,
Class[] parameterTypes,
Object target) {
EvaluationContext evaluationContext = expressionEvaluator.createEvaluationContext(className, methodName, classType, method, args, parameterTypes, target);
AnnotatedElementKey methodKey = new AnnotatedElementKey(method, classType);
return expressionEvaluator.getValueWithCustomerType(spElExpression, methodKey, evaluationContext, returnType);
}
/**
* 解析SpEll表达式
*
* @param spElExpression
* @param className
* @param methodName
* @param classType
* @param method
* @param args
* @param parameterTypes
* @param target
* @return
*/
public static Object getValue(String spElExpression,
String className,
String methodName,
Class classType,
Method method,
Object[] args,
Class[] parameterTypes,
Object target) {
EvaluationContext evaluationContext = expressionEvaluator.createEvaluationContext(className, methodName, classType, method, args, parameterTypes, target);
AnnotatedElementKey methodKey = new AnnotatedElementKey(method, classType);
return expressionEvaluator.getValue(spElExpression, methodKey, evaluationContext);
}
/**
* 解析SpEl表达式
*
* @param spElExpression
* @param className
* @param methodName
* @param classType
* @param method
* @param args
* @param parameterTypes
* @param target
* @return
*/
public static String getStringValue(String spElExpression,
String className,
String methodName,
Class classType,
Method method,
Object[] args,
Class[] parameterTypes,
Object target) {
EvaluationContext evaluationContext = expressionEvaluator.createEvaluationContext(className, methodName, classType, method, args, parameterTypes, target);
AnnotatedElementKey methodKey = new AnnotatedElementKey(method, classType);
return expressionEvaluator.getValueWithStringType(spElExpression, methodKey, evaluationContext);
}
/**
* 表达式根对象
* 该对象主要支持以下表达式格式:
* #root.className
* #root.methodName
* #root.classType
* #root.method
* #root.args
* #root.parameterTypes
* #root.target
*/
@Data
@AllArgsConstructor
private static class RPanExpressionRootObject {
/**
* 切点方法所属类名称
*/
private String className;
/**
* 切点方法名称
*/
private String methodName;
/**
* 类的Class
*/
private Class classType;
/**
* 代理方法实体
*/
private Method method;
/**
* 切点方法传参
*/
private Object[] args;
/**
* 切点方法传参类型
*/
private Class[] parameterTypes;
/**
* 代理对象实体
*/
private Object target;
}
/**
* 表达式执行器对象
*/
@Data
private static class RPanExpressionEvaluator extends CachedExpressionEvaluator {
private final ParameterNameDiscoverer paramNameDiscoverer = new DefaultParameterNameDiscoverer();
private final Map<ExpressionKey, Expression> conditionCache = new ConcurrentHashMap<>(256);
private final Map<AnnotatedElementKey, Method> targetMethodCache = new ConcurrentHashMap<>(256);
/**
* 创建表达式执行器上下文对象
*
* @param className
* @param methodName
* @param classType
* @param method
* @param args
* @param parameterTypes
* @param target
* @return
*/
private EvaluationContext createEvaluationContext(String className,
String methodName,
Class classType,
Method method,
Object[] args,
Class[] parameterTypes,
Object target) {
Method targetMethod = getTargetMethod(classType, method);
RPanExpressionRootObject root = new RPanExpressionRootObject(className, methodName, classType, method, args, parameterTypes, target);
return new MethodBasedEvaluationContext(root, targetMethod, args, this.paramNameDiscoverer);
}
/**
* 表达式解析,解析结果自动转化成指定类型
*
* @param conditionExpression
* @param elementKey
* @param evalContext
* @param clazz
* @return
*/
public <T> T getValueWithCustomerType(String conditionExpression, AnnotatedElementKey elementKey, EvaluationContext evalContext, Class<T> clazz) {
return getExpression(this.conditionCache, elementKey, conditionExpression).getValue(evalContext, clazz);
}
/**
* 表达式解析,解析结果不自动转化
*
* @param conditionExpression
* @param elementKey
* @param evalContext
* @return
*/
public Object getValue(String conditionExpression, AnnotatedElementKey elementKey, EvaluationContext evalContext) {
Expression expression = getExpression(this.conditionCache, elementKey, conditionExpression);
return expression.getValue(evalContext);
}
/**
* 表达式解析,解析结果自动转化成String
*
* @param conditionExpression
* @param elementKey
* @param evalContext
* @return
*/
public String getValueWithStringType(String conditionExpression, AnnotatedElementKey elementKey, EvaluationContext evalContext) {
Object value = getValue(conditionExpression, elementKey, evalContext);
if (Objects.nonNull(value)) {
return value.toString();
}
return StringUtils.EMPTY;
}
/**
* 获取缓存的目标方法
*
* @param targetClass
* @param method
* @return
*/
private Method getTargetMethod(Class<?> targetClass, Method method) {
AnnotatedElementKey methodKey = new AnnotatedElementKey(method, targetClass);
Method targetMethod = this.targetMethodCache.get(methodKey);
if (targetMethod == null) {
targetMethod = AopUtils.getMostSpecificMethod(method, targetClass);
if (targetMethod == null) {
targetMethod = method;
}
this.targetMethodCache.put(methodKey, targetMethod);
}
return targetMethod;
}
}
}
接着实现这个父类定义了一个标准默认的生成锁的类(就是为了获取唯一的标识方法的东西,通过方法的上下文信息以及SPEL表达式解析出来的东西)
- 这段代码是一个具体的锁键生成器类
StandardKeyGenerator
,它继承自之前提到的抽象类AbstractKeyGenerator
。在这个类中,实现了抽象方法doGenerateKey()
,用于生成标准格式的锁键值。这样生成的锁键值能够唯一标识某个方法的调用,并且考虑了方法的参数和动态生成的值,以保证锁的精确性。 - 这段代码首先创建了一个列表
keyList
,用于存储生成的锁键值的各个部分。然后将类名和方法名添加到keyList
中,以构成锁的唯一标识的一部分。接着,获取切点方法的参数类型数组parameterTypes
,如果参数类型数组不为空,则将每个参数类型转换为字符串并添加到keyList
中;如果参数类型数组为空,则将字符串表示的Void.class
类型添加到keyList
中,表示该方法没有参数。随后,将 SpEL 表达式解析得到的键值对中的值添加到keyList
中。最后,将keyList
中的各个部分用逗号连接起来,形成最终的锁键值,并返回。
@Component
public class StandardKeyGenerator extends AbstractKeyGenerator{
/**
* 标准key的生成方法
* 生成格式:className:methodName:parameterType1:...:value:value:..
* @param lockContext
* @param keyValueMap
* @return
*/
@Override
protected String doGenerateKey(LockContext lockContext, Map<String, String> keyValueMap) {
List<String> keyList = Lists.newArrayList();
keyList.add(lockContext.getClassName());
keyList.add(lockContext.getMethodName());
Class[] parameterTypes = lockContext.getParameterTypes();
if(ArrayUtils.isNotEmpty(parameterTypes)){
Arrays.stream(parameterTypes).forEach(parameterType->{
keyList.add(parameterType.toString());
});
}else {
keyList.add(Void.class.toString());
}
Collection<String> values = keyValueMap.values();
if(CollectionUtils.isNotEmpty(values)){
values.stream().forEach(value->{
keyList.add(value);
});
}
return keyList.stream().collect(Collectors.joining(","));
}
}
如果需要更换生成key的规则,只需要实现抽象类,然后注解标注就行
@Lock(keyGenerator = YourCustomKeyGenerator.class)
public void yourMethod() {
// 方法体
}
最后最后,需要做注解的增强逻辑,定义一个切面类
-
setApplicationContext(ApplicationContext applicationContext)
: 实现了ApplicationContextAware
接口,用于获取 Spring 应用程序上下文对象。在这个方法中,将应用程序上下文对象赋值给类的私有成员变量applicationContext
。 -
lockPointCut()
: 定义了一个切入点,用于匹配带有@Lock
注解的方法。 -
arroundLock(ProceedingJoinPoint proceedingJoinPoint)
: 定义了一个环绕通知,用于在带有@Lock
注解的方法执行前后进行增强处理。在该方法中:- 首先,调用
LockContext.init(proceedingJoinPoint)
方法初始化锁上下文信息。 - 然后,调用
checkAndGetLock(lockContext)
方法检查配置信息并获取锁实体。 - 接着,尝试获取锁并执行带锁的方法,如果获取锁成功,则调用
proceedingJoinPoint.proceed(args)
方法执行目标方法。 - 最后,在 finally 块中释放锁。
- 首先,调用
-
checkAndGetLock(LockContext lockContext)
: 检查上下文的配置信息,根据配置信息获取对应的锁实体。在该方法中:- 首先,判断
lockRegistry
是否为空,如果为空,则打印错误日志并返回空。 - 然后,调用
getLockkey(lockContext)
方法获取锁的键。 - 最后,根据锁的键从
lockRegistry
中获取锁实体并返回。
- 首先,判断
-
getLockkey(LockContext lockContext)
: 获取锁的键。在该方法中:- 首先,通过
applicationContext.getBean(lockContext.getAnnotation().keyGenerator())
获取键生成器实例。 - 然后,调用键生成器的
generateKey(lockContext)
方法生成锁的键。 - 如果键生成器实例为空,则打印错误日志并返回空字符串。
- 首先,通过
@Component
@Aspect
@Slf4j
public class LockAspect implements ApplicationContextAware {
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@Autowired
private LockRegistry lockRegistry;
@Pointcut(value = "@annotation(com.yubin.pan.lock.core.annotation.Lock)")
public void lockPointCut(){
}
@Around("lockPointCut()")
public Object arroundLock(ProceedingJoinPoint proceedingJoinPoint){
Object result = null;
LockContext lockContext = LockContext.init(proceedingJoinPoint);
java.util.concurrent.locks.Lock lock = checkAndGetLock(lockContext);
if(Objects.isNull(lock)){
log.error("lock aspect get lock fail.");
throw new RPanFrameworkException("arroundLock get lock fail");
}
boolean lockResult = false;
try {
lockResult = lock.tryLock(lockContext.getAnnotation().expireSecond(), TimeUnit.SECONDS);
if(lockResult){
Object[] args = proceedingJoinPoint.getArgs();
result = proceedingJoinPoint.proceed(args);
}
}catch (InterruptedException e){
log.error("lock aspect tryLock exception..",e);
throw new RPanFrameworkException("around Lock tryLock fail");
} catch (Throwable e) {
log.error("lock aspect tryLock exception..",e);
throw new RPanFrameworkException("around Lock tryLock fail");
}finally {
if(lockResult){
lock.unlock();
}
}
return result;
}
/**
* 检查上下文的配置信息,返回锁实体
* @param lockContext
* @return
*/
private java.util.concurrent.locks.Lock checkAndGetLock(LockContext lockContext) {
if(Objects.isNull(lockRegistry)){
log.error("the lockRegistry is not found...");
return null;
}
String lockKey = getLockkey(lockContext);
if(StringUtils.isEmpty(lockKey)){
return null;
}
java.util.concurrent.locks.Lock lock = lockRegistry.obtain(lockKey);
return lock;
}
/**
* 获取锁key的私有方法
* @param lockContext
* @return
*/
private String getLockkey(LockContext lockContext) {
KeyGenerator keyGenerator = applicationContext.getBean(lockContext.getAnnotation().keyGenerator());
if(Objects.nonNull(keyGenerator)){
return keyGenerator.generateKey(lockContext);
}
log.error("the keyGenerator is not found...");
return StringUtils.EMPTY;
}
}
测试类,多线程执行,在对应方法上面加注解
@Test
public void lockTesterTest() throws InterruptedException{
CountDownLatch countDownLatch = new CountDownLatch(10);
for (int i = 0; i < 10; i++) {
threadPoolTaskExecutor.execute(()->{
localTester.testLock("imooc");
countDownLatch.countDown();
});
}
countDownLatch.await();
}
@Component
public class LocalTester {
@Lock(name = "test" ,keys = "#name",expireSecond = 10L)
public String testLock(String name){
System.out.println(Thread.currentThread().getName() + " get the lock.");
String result = "hello " + name;
System.out.println(Thread.currentThread().getName() + " release the lock.");
return result;
}
}
结果符合预期