Java 分布式锁实现的一些实践
近期换工作,闲下来有点时间写点东西,在这里分享一些心得体会
背景:我们在做后端开发时,无法避免的会遇到一些一致性问题,有时候我们前端的小伙伴或者rpc接口的调用方,在很短的时间间隔内给我们相同的请求,由此可能会导致一些无法预见的问题,因此需要我们在接口层面处理,下面给大家分享一下我解决此类问题的一些实践
思路:1、加锁解锁的逻辑与正常的业务逻辑需要分开,不能耦合,否则会增加后期接口的维护成本,考虑使用自定义注解+aop;
2、锁的实现方式有很多,根据我们不同的场景和条件有不同的选择,因此加锁和解锁需要进行抽象,aop中只依赖锁的接口;
3、加锁时,我们需要生成key,用来区分是不是同一个请求,这里key的生成比较关键,不同的业务场景key的生成差别很大,我的想法是将这部分key生成逻辑抽象化,定义成接口,后面根据接口入参情况自己实现,当然也可以自己约定规则,通过反射去拼接;
实现步骤:
1 自定义注解:
/**
* 锁注解 标记需要加锁的接口
* @author XuZhangxing
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LockMask {
// 锁超时释放时间
long expireSecond() default 10; // 锁key的前缀
LockKeyPrefix lockKeyPre() default LockKeyPrefix.WMD; // 方法参数位置默认是0 第一个
int index() default 0; // el表达式 可以用来解析key
String el() default ""; }
2 锁对象dto
@Data
public final class LockParam {
// 锁的key
private String key;
// 针对一些特殊场景用
private String val;
// 超时时间 单位为秒
private long expireSecond;
// 加锁是否成功,用来在finally里面释放锁用
private Boolean success = Boolean.FALSE; public LockParam(String key, long expireSecond) {
this.key = key;
this.expireSecond = expireSecond;
} public LockParam() {
} @Override
public String toString() {
return JsonUtil.obj2String(this);
}
}
/**
* 锁key前缀
* 标记哪个业务系统
*
* @author XuZhangxing
*/
public enum LockKeyPrefix {
WMD
}
3 key生成抽象接口
/**
* * key生成 为了简化操作,直接由调用方去实现key的生成
*
* @author XuZhangxing
*/
@FunctionalInterface
public interface KeyGenerate { // 给key使用的 list 支持粒度更细的锁
List<String> generate();
}
4 实现的aop
package com.xzx.statistics.redis.lock; import com.google.common.collect.Lists;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component; import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors; /**
* 锁切面
*
* @author XuZhangxing
*/
@Aspect
@Component
@Order(1)
public final class LockAspect { private final LockService lockService; @Autowired(required = false)
public LockAspect(LockService lockService) {
this.lockService = lockService;
} // 锁的目标对象
@Pointcut("@annotation(com.xzx.statistics.redis.lock.LockMask)")
public void lockTargetMethod() {
} @Around("lockTargetMethod()")
public Object doLock(ProceedingJoinPoint joinPoint) throws Throwable { // 获取到方法签名
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); // 获取方法目标方法名称
Method targetMethod = methodSignature.getMethod(); // 获取实现类方法
Method targetImplMethod =
joinPoint.getTarget().getClass().getMethod(targetMethod.getName(), targetMethod.getParameterTypes()); // 获取到注释对象
LockMask lockMask = targetImplMethod.getAnnotation(LockMask.class); // 获取到目标对象
Object targetObj = joinPoint.getArgs()[lockMask.index()]; if (targetObj == null) {
return joinPoint.proceed();
} // 这里处理两种情况 一种基本数据类型的数据 二实现了KeyGenerate的对象,其他的都不处理
List<LockParam> lockParamList = getLockKeyList(targetObj, lockMask); if (lockParamList.size() < 1) {
return joinPoint.proceed();
}
// 加上分布式锁
lockService.lock(lockParamList);
try {
// 正常业务逻辑 这里可以考虑启动一个异步线程来给当前的锁续期
// 我觉得这样处理不太合理,不如直接将锁超时时间延长一些,有可能这个方法执行一直阻塞,锁无法释放
return joinPoint.proceed();
} finally {
// 释放锁
lockService.unLock(lockParamList); }
} // 抽取锁的key
private List<LockParam> getLockKeyList(Object target, LockMask lockMask) {
String el = lockMask.el();
if (el.trim().length() > 0) {
// 适配el 表达式 暂时不支持
throw new RuntimeException("function not supposed");
}
return getLockParams(target, lockMask);
} // 递归遍历锁key
private List<LockParam> getLockParams(Object target, LockMask lockMask) {
LockKeyPrefix lockKeyPrefix = lockMask.lockKeyPre();
long expireSecond = lockMask.expireSecond();
if (target == null || target.getClass().isPrimitive()
|| target.getClass().isAssignableFrom(String.class)) {
return Lists.newArrayList(new LockParam(lockKeyPrefix + ":" + target, expireSecond));
}
if (target instanceof KeyGenerate) {
KeyGenerate keyGenerate = (KeyGenerate) target;
// 注意对key去重
List<String> keyList = keyGenerate.generate().stream().distinct().collect(Collectors.toList());
return keyList.stream().map(key -> new LockParam(lockKeyPrefix + ":" + key, expireSecond))
.collect(Collectors.toList());
}
List<LockParam> result = Lists.newArrayList();
if (target.getClass().isArray()) {
Object[] arrayObj = (Object[]) target;
Arrays.stream(arrayObj).filter(Objects::nonNull)
.forEach(obj -> result.addAll(getLockParams(obj, lockMask)));
} else if (Iterable.class.isAssignableFrom(target.getClass())) {
Iterable<?> iterable = (Iterable<?>) target;
iterable.forEach(it -> result.addAll(getLockParams(it, lockMask))); }
return result;
} }
总结:
1 锁的实现方式有多种,比如可以使用数据库、redis、zookeeper 等
2 入参是list<String> 这种类型 或list<基本数据类型包装类>,也能适配,可以直接对整个list 加锁
3 最佳实践:建议将方法入参定义成对象,该对象实现 KeyGenerate 接口 ,这样复杂的入参也能由自己掌握key的生成规则,缺点是侵入性强,需要修改请求入参的dto
Java 分布式锁实现的一些实践的更多相关文章
- Java分布式锁之数据库实现
之前的文章<Java分布式锁实现>中列举了分布式锁的3种实现方式,分别是基于数据库实现,基于缓存实现和基于zookeeper实现.三种实现方式各有可取之处,本篇文章就详细讲解一下Java分 ...
- Java分布式锁之数据库方式实现
之前的文章<Java分布式锁实现>中列举了分布式锁的3种实现方式,分别是基于数据库实现,基于缓存实现和基于zookeeper实现.三种实现方式各有可取之处,本篇文章就详细讲解一下Java分 ...
- 基于redis的分布式锁的分析与实践
前言:在分布式环境中,我们经常使用锁来进行并发控制,锁可分为乐观锁和悲观锁,基于数据库版本戳的实现是乐观锁,基于redis或zookeeper的实现可认为是悲观锁了.乐观锁和悲观锁最根本的区别在于 ...
- Java分布式锁实现详解
在进行大型网站技术架构设计以及业务实现的过程中,多少都会遇到需要使用分布式锁的情况.那么问题也就接踵而至,哪种分布式锁更适合我们的项目? 下面就这个问题,我做了一些分析: 分布式锁现状: 目前几乎很多 ...
- Java分布式锁,搞懂分布式锁实现看这篇文章就对了
随着微处理机技术的发展,人们只需花几百美元就能买到一个CPU芯片,这个芯片每秒钟执行的指令比80年代最大的大型机的处理机每秒钟所执行的指令还多.如果你愿意付出两倍的价钱,将得到同样的CPU,但它却以更 ...
- Java分布式锁看这篇就够了
### 什么是锁? 在单进程的系统中,当存在多个线程可以同时改变某个变量(可变共享变量)时,就需要对变量或代码块做同步,使其在修改这种变量时能够线性执行消除并发修改变量. 而同步的本质是通过锁来实现的 ...
- Java分布式锁
分布式锁简述 在单机时代,虽然不存在分布式锁,但也会面临资源互斥的情况,只不过在单机的情况下,如果有多个线程要同时访问某个共享资源的时候,我们可以采用线程间加锁的机制,即当某个线程获取到这个资源后,就 ...
- Java分布式锁的三种实现方案(redis)
方案一:数据库乐观锁 乐观锁通常实现基于数据版本(version)的记录机制实现的,比如有一张红包表(t_bonus),有一个字段(left_count)记录礼物的剩余个数,用户每领取一个奖品,对应的 ...
- java 分布式锁 -图解- 秒懂
目录 写在前面 1.1. 分布式锁 简介 1.1.1. 图解:公平锁和可重入锁 模型 1.1.2. 图解: zookeeper分布式锁的原理 1.1.3. 分布式锁的基本流程 1.1.4. 加锁的实现 ...
- java 分布式锁
转自:http://www.hollischuang.com/archives/1716 目前几乎很多大型网站及应用都是分布式部署的,分布式场景中的数据一致性问题一直是一个比较重要的话题.分布式的CA ...
随机推荐
- Linux命令执行时,提示commond not found的处理办法
执行chattr命令时,或其他命令时,若出现命令没发现,commond not found 那么就是有可能命令文件丢了,执行find / -name chattr,如果没有命令了,可以拷贝一个或yum ...
- RTFormer: Efficient Design for Real-Time Semantic Segmentation with Transformer概述
0.前言 相关资料: arxiv github 论文解读 论文基本信息: 发表时间:NeurlPS2022(2022.10.13) 1.摘要 最近,基于Transformer的网络在语义分割方面取得了 ...
- vue 2 中防抖节流在当前页面里写
isfilter(val) { // 过滤 this.debounce(() => { this.init(val); }, 1000); ...
- cmake:macro,function中ARGV,ARGN参数的区别
cmake中的宏(macro)和函数(function)都支持动态参数 变量ARGC记录传入的参数个数 变量ARGV0,ARGV1,...顺序代表传入的参数 变量ARGV则是一个包含所有传入参数的li ...
- 【面试题】XSS攻击是什么?
XSS攻击是什么? 要点: XSS是跨站脚本攻击.向目标网站插入恶意代码.大量用户访问网站时运行恶意脚本获取信息 答: XSS是跨站脚本攻击(Cross Site Scripting),不写为CSS是 ...
- 安全测试-WEB安全渗透测试基础知识(五)
1.5. 代码审计 1.5.1. 简介 代码审计是找到应用缺陷的过程.其通常有白盒.黑盒.灰盒等方式.白盒指通过对源代码的分析找到应用缺陷,黑盒通常不涉及到源代码,多使用模糊测试的方式,而灰盒则是黑白 ...
- SAR成像(六):距离徙动矫正
1.什么是距离徙动? 距离徙动产生原因是合成孔径雷达的运动平台与目标之间的相对运动.对于地面一静止的点目标,当它处于雷达波束照射范围内的时间中时,随着飞行载体的运动,它与雷达飞行载体之间的距离会不断的 ...
- kafka数据顺序一致
问题: kafka如何发送顺序消息 方案:kafka可以通过partitionKey,将某类消息写入同一个partition,一个partition只能对应一个消费线程,以保证数据有序. 也就是说生产 ...
- 泛微e-cology OA 远程代码执行漏洞复现
复现过程: 直接使用FOFA搜索语法搜索,找了好久才找到含有漏洞的站点. 在根目录后面输入漏洞路径: /weaver/bsh.servlet.BshServlet 进入BeanShell操作页面 在S ...
- Servlet中使用request转发页面引发的500空指针异常
多余的不说如果你出现了我下面这张图的异常错误,可以继续看下去: 上面的错误是因为我们使用的转发地址错误引起的,这样他转发的地址不存在,则会出现空指针异常. register.jsp在我的Tomcat的 ...