赞
踩
幂等性:就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用
这边小编的采用的是AOP+Redis来处理的。
在前端处理,其实都是防君子不放小人。
1. 前端重复提交
用户注册,新增信息等操作,前端都会提交一些数据给后台服务,后台需要根据用户提交的数据在数据库中创建记录。如果用户不小心多点了几次,后端收到了好几次提交,这时就会在数据库中重复创建了多条记录。这就是接口没有幂等性带来的 bug
2. 接口超时重试
对于给第三方调用的接口,有可能会因为网络原因而调用失败,这时,一般在设计的时候会对接口调用加上失败重试的机制。
如果第一次调用已经执行了一半时,发生了网络异常。这时再次调用时就会因为脏数据的存在而出现调用异常。
3. 消息重复消费
在使用消息中间件来处理消息队列,且手动 ack 确认消息被正常消费时。如果消费者突然断开连接,那么已经执行了一半的消息会重新放回队列。
当消息被其他消费者重新消费时,如果没有幂等性,就会导致消息重复消费时结果异常,如数据库重复数据,数据库数据冲突,资源重复等。
如图:
01、全局唯一 ID 可以用百度的 uid-generator、美团的 Leaf 去生成
02、还要注意redis的原子性问题
1、先删除 token 还是后删除 token
2、Token 获取、比较和删除必须是原子性
如图:
如图:
在方法执行之前,先判断此业务是否已经执行过,如果执行过则不再执行,否则就正常执行。
提及将数据存储在内存中,最简单的方法就是使用 HashMap 存储,HashMap 的防重(防止重复)版本也是最原始的 。
缺点是 HashMap 是无限增长的,因此它会占用越来越多的内存,并且随着 HashMap 数量的增加查找的速度也会降低,已不再推荐。
使用最新的单例中著名的 DCL(Double Checked Locking,双重检测锁)来防止重复提交。
简而言之就是将执行的接口,存入map中,来进行处理
Apache 提供了一个 commons-collections 的框架,里面有一个数据结构 LRUMap
可以保存指定数量的固定的数据,并且它就会按照 LRU 的算法,帮你清除最不常用的数据。
悲观锁使用时一般伴随事务一起使用,数据锁定时间可能会很长,需要根据实际情况选用。
另外要注意的是,id 字段一定是主键或者唯一索引,不然可能造成锁表的结果,处理起来会非常麻烦。
这种方法适合在更新的场景中,版本号机制
附上lua脚本和使用方法
//脚本内容 -get/del
if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
//原子验证token和删除token
Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList('redis的key值'), '要校验的值');
if (result == 0L) {
//失败
}eles{
//校验成功-并且删除成功
}
分布式的话采用redis+redisson
依赖
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--aop依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
配置
# redis
spring.redis.host=127.0.0.1
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
#spring.redis.password=
package sqy.aop_test.vo; /** * @author suqinyi * @Date 2022/1/7 * API返回参数 */ public class ApiResult { private Integer code;//响应码 private String message;//消息内容 private Object data;//响应中的数据 private static Integer failCode =20000; private static Integer okCode =10000; //--------失败---------- public static ApiResult fail(String message) { return new ApiResult(failCode, message, null); } public static ApiResult fail(Integer code, String message) { return new ApiResult(code, message, null); } //--------成功---------- public static ApiResult ok(String message) { return new ApiResult(okCode, message, null); } public static ApiResult ok() { return new ApiResult(okCode, "成功", null); } public static ApiResult ok(String message, Object data) { return new ApiResult(okCode, message, data); } public static ApiResult ok(Integer code, String message, Object data) { return new ApiResult(code, message, data); } //--------构造/get/set---------- public ApiResult() { } public ApiResult(Integer code, String msg, Object data) { this.code = code; this.message = msg; this.data = data; } public Integer getCode() { return code; } public void setCode(Integer code) { this.code = code; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public Object getData() { return data; } public void setData(Object data) { this.data = data; } }
package sqy.aop_test.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * @author suqinyi * @Date 2022/1/7 * 重复提交 */ @Target(ElementType.METHOD)// 作用到方法上 @Retention(RetentionPolicy.RUNTIME)// 运行时有效 public @interface repeatApply { /** * 默认时间3秒 */ int applyTime() default 3 * 1000; }
存入redis,用时间来校验
这边获取ip的工具类就不贴出来了,自行百度一个,网上都有
package sqy.aop_test.aspect; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import sqy.aop_test.annotation.repeatApply; import sqy.aop_test.utils.IpUtils; import sqy.aop_test.vo.ApiResult; import javax.servlet.http.HttpServletRequest; import java.util.Objects; import java.util.concurrent.TimeUnit; /** * @author suqinyi * @Date 2021/12/27 * 接口重复提交 */ @Component @Aspect public class repeatApplyAspect { @Autowired StringRedisTemplate redisTemplate;//如果是分布式就用分布式锁 [Redisson 分布式锁] /** * @param joinPoint 切入点对象 ProceedingJoinPoint可以获取当前方法和参数等信息 * @return 使用@Around环绕通知, 环绕通知=前置通知+目标方法执行+后置通知 * 这边的返回值为controller的返回值 */ @Around("@annotation(repeatApply)") public Object doAround(ProceedingJoinPoint joinPoint, repeatApply repeatApply) { try { String redisKeyName = "apply:";//redis前缀 long lapseTime = 5;//失效时间 TimeUnit timeUnit = TimeUnit.SECONDS;//秒 HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest(); // 拿到ip地址、请求路径、token、方法名称 String ip = IpUtils.getIpAdrress(request); String url = request.getRequestURL().toString(); String token = request.getHeader("Token"); String methodName = joinPoint.getSignature().getName(); System.out.println("ip:" + ip + " " + "url:" + url + " " + "token:" + token + " " + "methodName:" + methodName); //安全校验等。。。。 // 现在时间 long now = System.currentTimeMillis(); // 自定义key值方式 String key = redisKeyName + ip + methodName; //是否存在key值 Boolean flag = redisTemplate.hasKey(key); if (flag) { // 上次提交时间 long lastTime = Long.parseLong(redisTemplate.opsForValue().get(key)); // 如果现在距离上次提交时间小于设置的默认时间 则 判断为重复提交 否则 正常提交 -> 进入业务处理 if ((now - lastTime) > repeatApply.applyTime()) { // 非重复提交操作 - 重新记录操作时间 redisTemplate.opsForValue().set(key, String.valueOf(now), lapseTime, timeUnit); // 进入处理业务-接收返回的结果 ApiResult result = (ApiResult) joinPoint.proceed(); return result; } else { return ApiResult.fail("请勿重复提交"); } } else { // JedisConnectionFactory factory =(JedisConnectionFactory) redisTemplate.getConnectionFactory(); // factory.setDatabase("2");//切换db // redisTemplate.setConnectionFactory(factory); // 这里是第一次请求 redisTemplate.opsForValue().set(key, String.valueOf(now), lapseTime, timeUnit); ApiResult result = (ApiResult) joinPoint.proceed(); return result; } } catch (Throwable e) { System.out.println("校验是否重复提交时异常: " + e.getMessage()); return ApiResult.fail("校验是否重复提交时异常"); } } }
package sqy.aop_test.controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import sqy.aop_test.annotation.repeatApply; import sqy.aop_test.vo.ApiResult; /** * @author suqinyi * @Date 2021/12/27 * 重复提交的controller */ @RestController public class RepeatApplyController { // http://localhost:8080/testApply3s @repeatApply//默认3秒 @RequestMapping(value = "/testApply3s", produces = "application/json;charset=utf-8") public ApiResult testApply3s() throws Exception { System.out.println("执行了方法-3秒内"); return ApiResult.ok("请求成功3秒的"); } // http://localhost:8080/testApply3s @repeatApply(applyTime = 1 * 1000)//1秒 @GetMapping(value = "/testApply1s") public ApiResult testApply1s() throws Exception { System.out.println("执行了方法-1秒内"); return ApiResult.ok("请求成功1秒的"); } // http://localhost:8080/testApply5s @repeatApply(applyTime = 5 * 1000)//5秒 @GetMapping(value = "/testApply5s") public ApiResult testApply5s() throws Exception { System.out.println("执行了方法-5秒内"); return ApiResult.ok("请求成功5秒的"); } }
如图:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。