• 接口防止重复提交,订单避免重复下单


    一.Java防止频繁请求、重复提交的操作代码(后端防抖操作)

    在客户端网络慢或者服务器响应慢时,用户有时是会频繁刷新页面或重复提交表单的,这样是会给服务器造成不小的负担的,同时在添加数据时有可能造成不必要的麻烦。所以我们在后端也有必要进行防抖操作。

    1.自定义注解

    1. /**
    2. * @author Tzeao
    3. */
    4. @Target(ElementType.METHOD) // 作用到方法上
    5. @Retention(RetentionPolicy.RUNTIME) // 运行时有效
    6. public @interface NoRepeatSubmit {
    7. //名称,如果不给就是要默认的
    8. String name() default "name";
    9. }

    2.使用AOP实现该注解

    1. /**
    2. * @author Tzeao
    3. */
    4. @Aspect
    5. @Component
    6. @Slf4j
    7. public class NoRepeatSubmitAop {
    8. @Autowired
    9. private RedisService redisService;
    10. /**
    11. * 切入点
    12. */
    13. @Pointcut("@annotation(com.qwt.part_time_admin_api.common.validation.NoRepeatSubmit)")
    14. public void pt() {
    15. }
    16. @Around("pt()")
    17. public Object arround(ProceedingJoinPoint joinPoint) throws Throwable {
    18. ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    19. assert attributes != null;
    20. HttpServletRequest request = attributes.getRequest();
    21. //这里是唯一标识 根据情况而定
    22. String key = "1" + "-" + request.getServletPath();
    23. // 如果缓存中有这个url视为重复提交
    24. if (!redisService.haskey(key)) {
    25. //通过,执行下一步
    26. Object o = joinPoint.proceed();
    27. //然后存入redis 并且设置15s倒计时
    28. redisService.setCacheObject(key, 0, 15, TimeUnit.SECONDS);
    29. //返回结果
    30. return o;
    31. } else {
    32. return Result.fail(400, "请勿重复提交或者操作过于频繁!");
    33. }
    34. }
    35. }

    3.serice,也可以放在工具包里面,这里我们使用到了Redis来对key和标识码进行存储和倒计时,所以在使用时还需要连接一下Redis

    1. package com.qwt.part_time_admin_api.service;
    2. import org.springframework.beans.factory.annotation.Autowired;
    3. import org.springframework.data.redis.core.*;
    4. import org.springframework.stereotype.Component;
    5. import java.util.*;
    6. import java.util.concurrent.TimeUnit;
    7. /**
    8. * @author Tzeao
    9. */
    10. @Component
    11. public class RedisService {
    12. @Autowired
    13. public RedisTemplate redisTemplate;
    14. /**
    15. * 缓存基本的对象,Integer、String、实体类等
    16. *
    17. * @param key 缓存的键值
    18. * @param value 缓存的值
    19. * @return 缓存的对象
    20. */
    21. public ValueOperations<String, T> setCacheObject(String key, T value) {
    22. ValueOperations<String, T> operation = redisTemplate.opsForValue();
    23. operation.set(key, value);
    24. return operation;
    25. }
    26. /**
    27. * 缓存基本的对象,Integer、String、实体类等
    28. *
    29. * @param key 缓存的键值
    30. * @param value 缓存的值
    31. * @param timeout 时间
    32. * @param timeUnit 时间颗粒度
    33. * @return 缓存的对象
    34. */
    35. public ValueOperations<String, T> setCacheObject(String key, T value, Integer timeout, TimeUnit timeUnit) {
    36. ValueOperations<String, T> operation = redisTemplate.opsForValue();
    37. operation.set(key, value, timeout, timeUnit);
    38. return operation;
    39. }
    40. /**
    41. * 获得缓存的基本对象。
    42. *
    43. * @param key 缓存键值
    44. * @return 缓存键值对应的数据
    45. */
    46. public T getCacheObject(String key) {
    47. ValueOperations<String, T> operation = redisTemplate.opsForValue();
    48. return operation.get(key);
    49. }
    50. /**
    51. * 删除单个对象
    52. *
    53. * @param key
    54. */
    55. public void deleteObject(String key) {
    56. redisTemplate.delete(key);
    57. }
    58. /**
    59. * 删除集合对象
    60. *
    61. * @param collection
    62. */
    63. public void deleteObject(Collection collection) {
    64. redisTemplate.delete(collection);
    65. }
    66. /**
    67. * 缓存List数据
    68. *
    69. * @param key 缓存的键值
    70. * @param dataList 待缓存的List数据
    71. * @return 缓存的对象
    72. */
    73. public ListOperations<String, T> setCacheList(String key, List dataList) {
    74. ListOperations listOperation = redisTemplate.opsForList();
    75. if (null != dataList) {
    76. int size = dataList.size();
    77. for (int i = 0; i < size; i++) {
    78. listOperation.leftPush(key, dataList.get(i));
    79. }
    80. }
    81. return listOperation;
    82. }
    83. /**
    84. * 获得缓存的list对象
    85. *
    86. * @param key 缓存的键值
    87. * @return 缓存键值对应的数据
    88. */
    89. public List getCacheList(String key) {
    90. List dataList = new ArrayList<>();
    91. ListOperations<String, T> listOperation = redisTemplate.opsForList();
    92. Long size = listOperation.size(key);
    93. for (int i = 0; i < size; i++) {
    94. dataList.add(listOperation.index(key, i));
    95. }
    96. return dataList;
    97. }
    98. /**
    99. * 缓存Set
    100. *
    101. * @param key 缓存键值
    102. * @param dataSet 缓存的数据
    103. * @return 缓存数据的对象
    104. */
    105. public BoundSetOperations<String, T> setCacheSet(String key, Set dataSet) {
    106. BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
    107. Iterator it = dataSet.iterator();
    108. while (it.hasNext()) {
    109. setOperation.add(it.next());
    110. }
    111. return setOperation;
    112. }
    113. /**
    114. * 获得缓存的set
    115. *
    116. * @param key
    117. * @return
    118. */
    119. public Set getCacheSet(String key) {
    120. Set dataSet = new HashSet<>();
    121. BoundSetOperations<String, T> operation = redisTemplate.boundSetOps(key);
    122. dataSet = operation.members();
    123. return dataSet;
    124. }
    125. /**
    126. * 缓存Map
    127. *
    128. * @param key
    129. * @param dataMap
    130. * @return
    131. */
    132. public HashOperations<String, String, T> setCacheMap(String key, Map<String, T> dataMap) {
    133. HashOperations hashOperations = redisTemplate.opsForHash();
    134. if (null != dataMap) {
    135. for (Map.Entry<String, T> entry : dataMap.entrySet()) {
    136. hashOperations.put(key, entry.getKey(), entry.getValue());
    137. }
    138. }
    139. return hashOperations;
    140. }
    141. /**
    142. * 获得缓存的Map
    143. *
    144. * @param key
    145. * @return
    146. */
    147. public Map<String, T> getCacheMap(String key) {
    148. Map<String, T> map = redisTemplate.opsForHash().entries(key);
    149. return map;
    150. }
    151. /**
    152. * 获得缓存的基本对象列表
    153. *
    154. * @param pattern 字符串前缀
    155. * @return 对象列表
    156. */
    157. public Collection<String> keys(String pattern) {
    158. return redisTemplate.keys(pattern);
    159. }
    160. /**
    161. * @param key
    162. * @return
    163. */
    164. public boolean haskey(String key) {
    165. return redisTemplate.hasKey(key);
    166. }
    167. public Long getExpire(String key) {
    168. return redisTemplate.getExpire(key);
    169. }
    170. public ValueOperations<String, T> setBillObject(String key, List<Map<String, Object>> value) {
    171. ValueOperations<String, T> operation = redisTemplate.opsForValue();
    172. operation.set(key, (T) value);
    173. return operation;
    174. }
    175. /**
    176. * 缓存list>
    177. *
    178. * @param key 缓存的键值
    179. * @param value 缓存的值
    180. * @param timeout 时间
    181. * @param timeUnit 时间颗粒度
    182. * @return 缓存的对象
    183. */
    184. public ValueOperations<String, T> setBillObject(String key, List<Map<String, Object>> value, Integer timeout, TimeUnit timeUnit) {
    185. ValueOperations<String, T> operation = redisTemplate.opsForValue();
    186. operation.set(key, (T) value, timeout, timeUnit);
    187. return operation;
    188. }
    189. /**
    190. * 缓存Map
    191. *
    192. * @param key
    193. * @param dataMap
    194. * @return
    195. */
    196. public HashOperations<String, String, T> setCKdBillMap(String key, Map<String, T> dataMap) {
    197. HashOperations hashOperations = redisTemplate.opsForHash();
    198. if (null != dataMap) {
    199. for (Map.Entry<String, T> entry : dataMap.entrySet()) {
    200. hashOperations.put(key, entry.getKey(), entry.getValue());
    201. }
    202. }
    203. return hashOperations;
    204. }
    205. }

    4.测试

    1. @NoRepeatSubmit(name = "test") // 也可以不给名字,这样就会走默认名字
    2. @GetMapping("test")
    3. public Result test() {
    4. return Result.success("测试阶段!");
    5. }

    15秒内重复点击就会给提示

     

    这样就完成了一个防止重复提交、频繁申请的程序!!!

    二: 订单避免重复下单

    1、重复原因
      造成重复下单的原因有很多,比如用户重复提交、网络超时导致的重试(网关超时重试、RPC超时重试,以及前端超时重试等),下单请求的整个链路都可能造成重复,大致可以分成如下三类:

            用户重复提交;
            恶意刷单;
            网络原因导致的超时重试;

    2、产品方案
      如何防止用户重复下单,并不只是技术的事情,因为技术并不一定能百分百的考虑到所有可能重复的场景,必须依靠产品+技术共同的努力,以及运营、客服等订单重复时的事后处理。

            用户点击”提交订单“按钮后,对按钮置灰,禁止再次提交;
            对于涉及金额比较大的订单,需要弹窗二次确认;
            对用户的下单频率、次数进行限制;

    3、技术方案
      用户下单在逻辑上是一个非幂等行为,因此解决方案都是基于物理去重的思路设计的,用某个id表示某次下单行为,通过该id判断是否重复;

    如何防止重复下单

    防止用户提交,最常规的做法,就是客户端点击下单之后,在收到服务端响应之前,按钮置灰。

    当然,防止重复下单,肯定不能只依靠客户端,可能会因为一些网络的抖动,导致仍然有重复的请求到达服务端,所以还是要在服务端做防重/幂等的处理。

    PS:这里额外插入一点我对防重和幂等的理解:防重指的是防止重复提交,幂等指的是多次请求如一次,简单说,就是防重可以给对重复请求抛异常,幂等是对重复的请求响应第一次的结果,在我们讨论的这个场景里,幂等就是响应唯一的订单号。

     

    防重第一步,需要识别请求是否重复,这一步,需要客户端配合实现。

    为什么呢?大家想一下,下单的时候,服务端怎么去判断这个下单请求是否唯一呢?金额?商品?优惠券?……万一用户就是喜欢,又下了一个一模一样的单呢?

    所以,需要客户端在请求下单接口的时候,需要生成一个唯一的请求号:requestId/唯一订单号,服务端拿这个请求号/订单号,判断是否重复请求。

    3.1 基于订单号去重
    用户进入下单页面时,前端页面先调用订单服务得到一个订单号;
    用户提交订单时,携带得到的订单号,向后端发送创建订单请求;
    接收到创建订单请求后,订单系统校验订单号是否合法,以及是否已被缓存;
    如果存在,说明订单合法,将订单号作为唯一主键在数据库中创建订单;

    参考:

    订单系统设计 —— 重复下单_库昊天的博客-CSDN博客_如何解决重复下单

    如何避免下重复订单为啥会下重了呢?用幂等防止重复订单客户端的流程后端数据表设计下单的实现技术搞定幂等就足够了吗?通知如果还拦不住……这么麻烦,有必要吗?结论 - 腾讯云开发者社区-腾讯云

  • 相关阅读:
    不规则间隔时间序列转规则时间序列
    文件包含笔记
    JAVA实现校园二手交易系统 开源
    RepViT:从ViT视角重新审视移动CNN
    c语言基础学习笔记(二):表达式和运算符优先级
    IDEA中查看整个项目代码行数
    从面向对象解读设计思想
    从底层结构开始学习FPGA----RAM IP核及关键参数介绍
    基于Echarts实现可视化数据大屏销售大数据分析系统
    C#:模式匹配与模式
  • 原文地址:https://blog.csdn.net/zhangleiyes123/article/details/126345928