订单重复提交
订单重复提交指的是同一个订单多次提交的情况,常见原因如下:
- 重复点击:用户可能会因为网络延迟、手抖、页面加载缓慢等原因,多次点击下单提交按钮
- 页面刷新:用户在订单提交后,如果刷新页面,浏览器可能会重新发送上一次的表单提交请求
防止订单重复提交的本质就是保证下单方法不要被重复调用,这首先可以从前端处理
比如在用户点击提交按钮后,立即将按钮禁用或者改为提交中状态,防止用户再次点击
但是前端只能提高用户的体验,不能保证安全,因为用户可以通过一些脚本提交请求,因此还需要后端处理
后端的处理方式有很多,经常用到的就是Token机制和分布式锁
Token机制
- 生成Token:用户每次进入下单页面,前端会向服务器请求一个唯一的Token
- 存储Token:后端将生成Token存放在Redis中,并设置一定的过期时间,然后给前端返回
- 验证Token:用户提交订单时需要携带Token,服务器验证Token,一旦Token被使用过就失效
分布式锁
分布式锁是在分布式系统下的锁机制,旨在保证代码在加锁期间只能被同一个线程访问
实现分布式锁的方案有很多,比如下面这些:
- MySQL:可以利用MySQL主键、唯一列不能保存重复数据这一特性做分布式锁
- Redis:可以利用setnx命令无法向Redis重复保存同一key的特性做分布式锁
Redisson
Redisson是一个高级的Redis客户端,借助它可以轻松的实现使用Redis做分布式锁、延迟消息队列等功能
相比于RedisTemplate之类的客户端,它更侧重于对Redis功能的模块化封装
API
RedissonClient是Redisson的客户端对象,核心方法如下:
- 定义锁:getLock(锁)
- 获取锁:tryLock(获取锁的超时时间, 持有锁的最大时间, 时间单位)
- 释放锁:unlock()
注意:释放锁的代码一定要写在finally中,保证锁一定可以得到释放
将资料中的demo-redisson.zip解压后导入到Idea中,打开测试类,学习它的核心API
- 创建客户端连接对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Configuration public class RedissonConfiguration { @Bean public RedissonClient redissonClient() { Config config = new Config(); config.useSingleServer() .setAddress("redis://192.168.101.68:6379") .setPassword("redis"); return Redisson.create(config); } }
|
- 在测试类中测试分布式锁的使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| @SpringBootTest public class RedissonTest {
@Autowired private RedissonClient redissonClient;
@Test public void test() { for (int i = 0; i < 3; i++) { new Thread(() -> { RLock lock = redissonClient.getLock("LOCK"); try { boolean flag = lock.tryLock(3, 10, TimeUnit.SECONDS); if (!flag) { throw new RuntimeException("请求太频繁"); } System.out.println(Thread.currentThread().getId() + "执行逻辑"); ThreadUtil.sleep(1, TimeUnit.MINUTES); } catch (Exception e) { System.out.println(Thread.currentThread().getId() + "抢锁失败"); } finally { lock.unlock(); } }).start(); }
ThreadUtil.sleep(30000000); } }
|
实现防重复下单
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| @Autowired private RedissonClient redissonClient;
@GetMapping("/placeOrder") public void placeOrder(){ RLock lock = redissonClient.getLock("LOCK"); try { boolean flag = lock.tryLock(1, 30, TimeUnit.SECONDS); if (!flag) { throw new RuntimeException("请求太频繁"); }
System.out.println(Thread.currentThread().getId() + "下单成功"); ThreadUtil.sleep(3000);
} catch (Exception e) { System.out.println(Thread.currentThread().getId() + "下单失败"); } finally { } }
|
AOP优化代码
上一步的代码虽然可以实现防止重复下单的功能,但是代码繁琐,可以考虑使用AOP的方式来进行优化
当前提供的工程中已经提供好了一个切面类LockAspect,在类中实现的加锁和解锁的逻辑
直接在方法中使用它来简化下单的方法,具体实现如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @RestController public class OrderController {
@Lock(formatter = "Lock2", waitTime = 1, time = 30, unlock = false) @GetMapping("/placeOrder") public void placeOrder() { System.out.println(Thread.currentThread().getId() + "下单成功"); ThreadUtil.sleep(3000); } }
|
AOP:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
| package com.heima.redisson.aspect;
import com.heima.redisson.annotation.Lock; import com.heima.redisson.util.AspectUtils; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
@Aspect @Component public class LockAspect {
@Autowired private RedissonClient redissonClient;
@Around("@annotation(lock)") public Object handleLock(ProceedingJoinPoint pjp, Lock lock) throws Throwable { String formatter = lock.formatter(); Method method = AspectUtils.getMethod(pjp); Object[] args = pjp.getArgs(); String redisKey = AspectUtils.parse(formatter, method, args); long waitTime = 0; if (lock.block()) { waitTime = lock.waitTime(); } long time = lock.time(); if (lock.startDog()) { time = -1; if (!lock.unlock()) { throw new RuntimeException("请求参数不合法"); } } RLock rLock = redissonClient.getLock(redisKey); boolean success = rLock.tryLock(waitTime, time, lock.unit()); if (!success && !lock.block()) { throw new RuntimeException("操作频繁,请稍后重试"); } if (!success) { throw new RuntimeException("请求超时"); }
try { return pjp.proceed(); } finally { if (lock.unlock()) { rLock.unlock(); } } }
}
|