百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程网 > 正文

基于分布式锁的防止重复请求解决方案(值得收藏)

yuyutoo 2024-10-13 00:33 1 浏览 0 评论

关于重复请求,指的是我们服务端接收到很短的时间内的多个相同内容的重复请求。而这样的重复请求如果是幂等的(每次请求的结果都相同,如查询请求),那其实对于我们没有什么影响,但如果是非幂等的(每次请求都会对关键数据造成影响,如删除关系、建立关系等),那就会轻则产生脏数据,重则导致系统错误。

因此,在当前普遍分布式服务的情况下,如何避免和解决重复请求给我们带来的数据异常成为了亟待解决的问题。而避免重复请求,最好的做法是前后端共同去做。

1. 前端或客户端在非幂等的按钮上直接做禁止提交重复请求的操作。

2. 后端在接收到请求时加锁,完成后解锁。

这篇博客主要讲的是在后端基于分布式锁的概念去出一个关于解决重复请求的通用解决方案。

二、正文

为何要使用分布式锁来解决呢?因为我们当前普遍的架构都是分布式的服务端,前端请求通过网关层转发至后端,如下图所示,因此如果只在一个单独的服务器上做限制,就无法在分布式的服务中完成应对高频次的重复请求了。

基本思路

思路基本上是对需要做防止重复请求的接口加上分布式锁,步骤如下:

  1. 在接收到请求后,根据方法名+参数取md5值,获取该方法及该参数的唯一标识;
  2. 获取标识后设置分布式锁,并且设置过期时间;
  3. 在请求结束后,释放分布式锁。

即可完成对当前请求的重复请求禁止。如果想做通用的解决方案,那就需要把上述步骤做出一个小功能出来,由于本人对java、spring框架比较熟悉,就拿这个来做个示例。

基于spring切面、redis的实现

想必一些熟悉spring的同学已经知道我想采用什么方式了,做通用型的,肯定要用到spring的aop特性,注解+切面+md5key+反射+redis实现,具体如下:

  1. 定义一个分布式锁注解,注解包含过期时间设置、忽略参数;
  2. 定义一个切面,切点为分布式锁注解,在切面中获取需要使用分布式锁的方法名、参数、过期时间,并且将方法名及未被忽略参数做md5取唯一标识;
  3. 再根据上述唯一标识设置redsis分布式锁;
  4. 方法结束后解锁。

代码如下:

注解

定义名称为RepeatOperationLock的注解,参数有锁过期时间及忽略属性(即不参与分布式锁标识MD5计算的属性)。

@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Component
public @interface RepeatOperationLock {
 /**
 * 锁时长,默认500ms
 * @return
 */
 long timeOut() default 500;
 /**
 * 忽略上锁参数位置,从0开始
 * @return
 */
 int[] ignoreIndex();
}
复制代码

切面

切点为上述注解,切面中做了以下几件事,获取方法名、获取注解属性(过期时间、忽略属性)、计算方法+属性的md5值、调用外部分布式锁的方法。

@Aspect
@Slf4j
@Component
public class LockAspect {
 @Autowired
 RepeatLockService repeatLockService;
 @Pointcut("@annotation(com.ls.javabase.aspect.annotation.RepeatOperationLock)")
 public void serviceAspect() {
 }
 @Before("serviceAspect()")
 public void setLock(JoinPoint point) {
 log.info("防止方法重复调用接口锁,上锁,point:{}", point);
 Method method = ((MethodSignature) point.getSignature()).getMethod();
 RepeatOperationLock repeatOperationLock = method.getAnnotation(RepeatOperationLock.class);
 if (Objects.isNull(repeatOperationLock)) {
 log.warn("---repeatOperationLock is null---");
 return;
 }
 long timeOut = repeatOperationLock.timeOut();
 int [] ignoreIndex = repeatOperationLock.ignoreIndex();
 log.info("lockTime——{}", timeOut);
 if (Objects.isNull(timeOut)) {
 log.warn("---timeOut is null");
 return;
 }
 String methodName = method.getName();
 Object[] args = point.getArgs();
 repeatLockService.setRepeatLock(methodName, args, timeOut);
 }
 @After("serviceAspect()")
 public void removeLock(JoinPoint point) {
 log.info("防止方法重复调用接口锁,解锁,point:{}",point);
 Method method = ((MethodSignature) point.getSignature()).getMethod();
 RepeatOperationLock repeatOperationLock = method.getAnnotation(RepeatOperationLock.class);
 if (Objects.isNull(repeatOperationLock)) {
 log.warn("---repeatOperationLock is null---");
 return;
 }
 long timeOut = repeatOperationLock.timeOut();
 log.info("lockTime——{}", timeOut);
 if (Objects.isNull(timeOut)) {
 log.warn("---timeOut is null");
 return;
 }
 String methodName = method.getName();
 Object[] args = point.getArgs();
 repeatLockService.removeRepeatLock(methodName, args);
 }
 /**
 *
 * @param args
 * @param ignoreIndex
 * @return
 */
 private Object [] getEffectiveArgs(Object[] args,int [] ignoreIndex) {
 for (int i:ignoreIndex){
 args[i] = null;
 }
 for (Object obj:args){
 if (obj==null){
 }
 }
 return args;
 }
}
复制代码

md5方法

public class Md5Encode {
 /**
 * constructors
 */
 private Md5Encode() {
 }
 /**
 * @param s 需要hash的字符串
 * @return hash之后的字符串
 */
 public static final String md5(final String s) {
 char[] hexDigits = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
 try {
 byte[] btInput = s.getBytes(Charset.defaultCharset());
 // 获得MD5摘要算法的 MessageDigest 对象
 MessageDigest mdInst = MessageDigest.getInstance("MD5");
 // 使用指定的字节更新摘要
 mdInst.update(btInput);
 // 获得密文
 byte[] md = mdInst.digest();
 // 把密文转换成十六进制的字符串形式
 int j = md.length;
 char[] str = new char[j * 2];
 int k = 0;
 for (int i = 0; i < j; i++) {
 byte byte0 = md[i];
 str[k++] = hexDigits[byte0 >>> 4 & 0xf];
 str[k++] = hexDigits[byte0 & 0xf];
 }
 return new String(str);
 } catch (Exception e) {
 e.printStackTrace();
 return null;
 }
 }
}
复制代码

分布式锁

这里的分布式锁使用redis,但有点问题,是在设置锁以及锁过期时间时不是原子性的,后续会做出改进,实现一个完整的分布式锁方案,写到博客里。

@Slf4j
@Service
public class RepeatLockService {
 @Autowired
 RepeatRedisUtil repeatRedisUtil;
 public void setRepeatLock(String methodName, Object[] args, Long expireTime) {
 for (Object obj : args) {
 log.info("方法名:{},对象:{},对象hashcode:{}", methodName, obj, obj.hashCode());
 }
 Boolean lock = repeatRedisUtil.setRepeatLock(methodName, args, expireTime);
 if (!lock) {
 log.info("已有相同请求");
 }
 }
 public void removeRepeatLock(String methodName, Object[] args) {
 repeatRedisUtil.removeRepeatLock(methodName, args);
 }
}
@Component
public class RepeatRedisUtil {
 @Autowired
 RedisTemplate redisTemplate;
 private static final String repeatLockPrefix = "repeat_lock_";
 /**
 * 设置重复请求锁,这一块的分布式锁的加与释放有问题,后续会专门出个文章总结redis分布式锁
 * @param methodName
 * @param args
 * @param expireTime 过期时间ms
 * @return
 */
 public boolean setRepeatLock(String methodName, Object[] args,long expireTime) {
 String key = getRepeatLockKey(methodName, args);
 ValueOperations val = redisTemplate.opsForValue();
 Long flag = val.increment(key, 1);
 if (flag != 1) {
 return false;
 }
 redisTemplate.expire(key, expireTime, TimeUnit.MILLISECONDS);
 return true;
 }
 /**
 * 删除重复请求锁
 * @param methodName
 * @param args
 */
 public void removeRepeatLock(String methodName, Object[] args){
 String key = getRepeatLockKey(methodName, args);
 redisTemplate.delete(key);
 }
 /**
 * 获取重复请求锁Key
 *
 * @param methodName
 * @param args
 * @return
 */
 public String getRepeatLockKey(String methodName, Object[] args) {
 String repeatLockKey = repeatLockPrefix + methodName;
 for (Object obj : args) {
 repeatLockKey = repeatLockKey+"_"+ obj.hashCode();
 }
 return repeatLockKey;
 }
}
复制代码

测试service接口

即在方法上使用注解即可,代表过期时间为200000ms,忽略第二个参数。

@Slf4j
@Service
public class TestLockService {
 @RepeatOperationLock(timeOut = 200000, ignoreIndex = 1)
 public void testLock(UserDto userDto,int i){
 log.info("service中属性:{},{}",userDto,i);
 log.info("service中hashcode,userDto:{},i:{}",userDto.hashCode(),i);
 }
}
复制代码

结语

这样一个基于spring的通用分布式锁解决方案就分享完毕了,确实还存在着一些瑕疵,在分布式锁的实现上海有一些问题,比如上锁时不是原子性的,解锁时没有判断是否会被误解等等,后续会专门作出分布式锁的总结并改进,上面也只是提出了一个基于分布式锁解决重复请求的思想,也希望能多多交流。

相关推荐

《亲爱的》特效真厉害,一块绿布贯彻全集,不得不佩服李现的演技

说起《亲爱的热爱的》这部电视剧,相信很多人并不陌生,这部电视剧让男演员李现因此爆红,其实李现是一个非常有潜力的男演员,他的表现也非常的精彩。但是大家都知道,在每一部电视剧中都有特效的存在,这部电视剧也...

充分运用AI特效,京剧电影《大唐贵妃》正式开机

“当唱到‘在天愿为比翼鸟,在地愿为连理枝’的时候,会从演员身后飞起一只巨大无比的比翼鸟,鸟在空中盘旋,刮起的风甚至会吹过角色的脸……”11月10日,电影《大唐贵妃》在上海车墩影视基地正式开机,这是第一...

工业光魔:没有PS的年代,特效怎么做?

大家好,我是戴着眼镜拿着话筒的阿拉斯加,片片。昨天,我给大家简单介绍了传奇特效公司——工业光魔的起源。...

价值13亿的“爽片”上映,每一帧都是钱砸出来的特效,却无缘国内

在七月二号的时候;美国在网络上面上映了一部大制作的“爽片”《明日之战》。...

影视特效是怎么制作出来的?来,一起体验吧!

电影是人们生活中不可缺少的一部分,是视觉与听觉相结合的现代艺术。悲剧、喜剧、亲情、友情、爱情都是它所能展现的内容。影视艺术有哪些独有的特征?什么是蒙太奇?全息影片又是什么意思?最近,少儿频道的一档节目...

ps五分钟学会自己制作“特效”

嗨,欢迎观看本期的摄影后期教学。那就是教大家一个特别炫酷,特别实用的ps“特效”光!有同学会问,“啊,特效光是不是特别难,我一点ps都不会!”咳咳不要着急,karry的教学都是针对ps零基础的同学的,...

抖音这个特效太扎心,无数网友看着看着就哭了……

2078年的你,会是什么样子?最近,抖音上一款特效火了。它可以让你看到自己从现在到2078年的容貌变化,你可以一点点看着皱纹爬上眼角,白发布满双鬓。看着看着,很多网友就哭了。有人说,一辈子很长却也很短...

《爱情公寓》的特效有多好?这集花了300万,网友看不出来特效

对于有些电视剧来说,特效那可是相当重要的一个部分了。尤其是对那些科幻片和一些玄幻类的电视剧就更加了。一部剧或者是影片特效的好坏,直接就影响了这个作品的质量。首先就让我们来看一下电视剧孤芳不自赏吧!里面...

窗花剪纸、鸭头滤镜、潜艇王者…… 揭秘抖音特效那些事儿

更多往期「Byte漫来了」系列漫画,可点击阅读:...

原来影视剧中的“光头”全是这样弄的 观众被骗了许多年

不管是电影还是电视剧,都会有光头因为是剧情的需要,那么她们的光头会是怎么弄的呢?一直都以为那些演员都是要真的剃光头,看到这些照片后,才知道深深的被骗了许多年。在影视中,常常看到演员有光头的戏,她在外出...

这些特效给几分?春晚舞台显示技术盘点

[中关村在线投影机频道原创]从1983年开始,春晚成为国人大年三十晚上最期待的一场精神盛宴。直播式节目播出方式,各类表演大师云集舞台,相声小品歌舞欢聚一堂,这是很多家庭一年中最欢乐的时刻,而春晚,则是...

11 款六元秒杀特效对比,哪个赢了不知道,貂蝉一眼输了

已经上线了11款六元秒杀皮肤,其中部分是升级后的“伴生皮”,也就是英雄的首款皮肤,首周六元之后恢复原价488点券。无论是何种形式的六元皮肤,不得不说他们的质量都非常好,这11款皮肤的特效对...

《半条命2 RTX》上架Steam 光追特效+4K效果

《半条命2RTX》现已在Steam平台正式上架,这款屡获殊荣的游戏以其沉浸式的故事、惊险的战斗和令人费解的物理特性吸引了全球数百万玩家,游戏现已经新增全面光追、物理纹理以及增强的多边形效果进行了彻底...

微信又有新玩法?多种好看特效可选择,赶快安排上

今日分享:手机特效适用系统:安卓今天小雷又发现了一个好玩的东西,就是手机加上特效也太好看了吧!早上上班的时候发现同事打开微信后,微信页面居然下起了樱花雨。看了同事的微信页面小雷我真的觉得太好玩太好看了...

判若两人!揭秘好莱坞的特效化妆过程

好莱坞的特效化妆牛到什么程度呢?可以把美人化妆成丑八怪,把黑人化妆成白人,把年轻人化妆成老人,把女人化妆成男人,把男人化妆成女人,把普通人化妆成怪物、外星人甚至异鬼……来见识一下这些神奇化妆术的“变...

取消回复欢迎 发表评论: