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

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

yuyutoo 2024-10-13 00:33 5 浏览 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的通用分布式锁解决方案就分享完毕了,确实还存在着一些瑕疵,在分布式锁的实现上海有一些问题,比如上锁时不是原子性的,解锁时没有判断是否会被误解等等,后续会专门作出分布式锁的总结并改进,上面也只是提出了一个基于分布式锁解决重复请求的思想,也希望能多多交流。

相关推荐

自卑的人容易患抑郁症吗?(自卑会导致抑郁吗)

Filephoto[Photo/IC]Lowself-esteemmakesusfeelbadaboutourselves.Butdidyouknowthatovert...

中考典型同(近)义词组(同义词考题)

中考典型同(近)义词组...

WPF 消息传递简明教程(wpf messagebox.show)

...

BroadcastReceiver的原理和使用(broadcast-suppression)

一、使用中注意的几点1.动态注册、静态注册的优先级在AndroidManifest.xml中静态注册的receiver比在代码中用registerReceiver动态注册的优先级要低。发送方在send...

Arduino通过串口透传ESP 13板与java程序交互

ESP13---是一个无线板子,配置通过热点通信Arduino通过串口透传ESP13板与java程序交互...

zookeeper的Leader选举源码解析(zookeeper角色选举角色包括)

作者:京东物流梁吉超zookeeper是一个分布式服务框架,主要解决分布式应用中常见的多种数据问题,例如集群管理,状态同步等。为解决这些问题zookeeper需要Leader选举进行保障数据的强一致...

接待外国人英文口语(接待外国友人的英语口语对话)

接待外国人英文口语询问访客身份:  MayIhaveyourname,please?  请问您贵姓?  Whatcompanyareyoufrom?  您是哪个公司的?  Could...

一文深入理解AP架构Nacos注册原理

Nacos简介Nacos是一款阿里巴巴开源用于管理分布式微服务的中间件,能够帮助开发人员快速实现动态服务发现、服务配置、服务元数据及流量管理等。这篇文章主要剖析一下Nacos作为注册中心时其服务注册与...

Android面试宝典之终极大招(android面试及答案)

以下内容来自兆隆IT云学院就业部,根据多年成功就业服务经验,以及职业素养课程部分内容,归纳总结:18.请描述一下Intent和IntentFilter。Android中通过Intent...

除了Crontab,Swoole Timer也可以实现定时任务的

一般的定时器是怎么实现的呢?我总结如下:1.使用Crontab工具,写一个shell脚本,在脚本中调用PHP文件,然后定期执行该脚本;2.ignore_user_abort()和set_time_li...

Spark源码阅读:DataFrame.collect 作业提交流程思维导图

本文分为两个部分:作业提交流程思维导图关键函数列表作业提交流程思维导图...

使用Xamarin和Visual Studio开发Android可穿戴设备应用

搭建开发环境我们需要做的第一件事情是安装必要的工具。因此,你需要首先安装VisualStudio。如果您使用的是VisualStudio2010,2012或2013,那么请确保它是一个专业版本或...

Android开发者必知的5个开源库(android 开发相关源码精编解析)

过去的时间里,Android开发逐步走向成熟,一个个与Android相关的开发工具也层出不穷。不过,在面对各种新鲜事物时,不要忘了那些我们每天使用的大量开源库。在这里,向大家介绍的就是,在这个任劳任怨...

Android事件总线还能怎么玩?(android实现事件处理的步骤)

顾名思义,AndroidEventBus是一个Android平台的事件总线框架,它简化了Activity、Fragment、Service等组件之间的交互,很大程度上降低了它们之间的耦合,使我们的代码...

Android 开发中文引导-应用小部件

应用小部件是可以嵌入其它应用(例如主屏幕)并收到定期更新的微型应用视图。这些视图在用户界面中被叫做小部件,并可以用应用小部件提供者发布。可以容纳其他应用部件的应用组件叫做应用部件的宿主(1)。下面的截...

取消回复欢迎 发表评论: