秒杀/高并发解决方案+落地实现 (技术栈: SpringBoot+Mysql + Redis +RabbitMQ +MyBatis-Plus +Maven + Linux + Jmeter ) -05
@
目录
- 秒杀/高并发解决方案+落地实现 (技术栈: SpringBoot+Mysql + Redis +RabbitMQ +MyBatis-Plus +Maven + Linux + Jmeter ) -05
- Redis 分布式锁探讨
- 用户名是手机号: 13300000000 密码为: 123456 加密
- 项目启动准备说明
- 最后:
- Github:China-Rainbow-sea/seckill: 秒杀/高并发解决方案+落地实现 (技术栈: SpringBoot+Mysql + Redis + RabbitMQ +MyBatis-Plus + Maven + Linux + Jmeter )
- Gitee:seckill: 秒杀/高并发解决方案+落地实现 (技术栈: SpringBoot+Mysql + Redis + RabbitMQ +MyBatis-Plus + Maven + Linux + Jmeter )
Redis 分布式锁探讨
分析:
我们在进行秒杀时,我们使用了一个关键的方法,找到对应的代码;
Redis 的单个 decrement方法具有原子性和隔离性,所以有效的控制了抢购。
所以在本项目中,不使用 Redis 分布式锁,也是可以控制抢购不出现超购和复购。
问题:
如果我们这里要处理的业务,不是当个 Redis 操作比如 decrement 可以完成的,而是需要多个 Redis 操作,那么就需要将多个操作组合起来,满足原子性了。
扩展:
在实际开发中,我们业务可能比较复杂综合,不是一个 Redis 操作(decrement) 就可以完成的,比如还需要进行修改操作(set),甚至还会操作 DB,文件,第三方数据源等等。
这时我们就需要扩大代码隔离性范围,可以考虑使用 Redis 分布式锁,解决。
修改:SeckillController 秒杀执行 decrement 进行一个 Redis 分布式锁处理。
- /** * 方法: 处理用户抢购请求/秒杀 * 说明: 我们先完成一个 V 7.0版本, * - 利用 MySQL默认的事务隔离级别【REPEATABLE-READ】 * - 使用 优化秒杀: Redis 预减库存+Decrement * - 优化秒杀: 加入内存标记,避免总到 Redis 查询库存 * - 优化秒杀: 加入消息队列,实现秒杀的异步请求 * - 优化: 扩展: 采用 Redis 分布式锁,控制事务 * * @param model 返回给模块的 model 信息 * @param user User 通过用户使用了,自定义参数解析器获取 User 对象, * @param goodsId 秒杀商品的 ID 信息 * @return 返回到映射在 resources 下的 templates 下的页面 */ @RequestMapping(value = "/doSeckill") public String doSeckill(Model model, User user, Long goodsId) { System.out.println("秒杀 V 7.0 "); if (user == null) {//用户没有登录 return "login"; } //将user放入到model, 下一个模板可以使用 model.addAttribute("user", user); //获取到goodsVo GoodsVo goodsVo = goodsService.findGoodsVoByGoodsId(goodsId); //判断库存 if (goodsVo.getStockCount() < 1) {//没有库存 model.addAttribute("errmsg", RespBeanEnum.ENTRY_STOCK.getMessage()); return "secKillFail";//错误页面 } //判断用户是否复购-直接到Redis中,获取对应的秒杀订单,如果有,则说明已经抢购了 SeckillOrder o = (SeckillOrder) redisTemplate.opsForValue() .get("order:" + user.getId() + ":" + goodsVo.getId()); if (null != o) { //说明该用户已经抢购了该商品 model.addAttribute("errmsg", RespBeanEnum.REPEAT_ERROR.getMessage()); return "secKillFail";//错误页面 } //对map进行判断[内存标记],如果商品在map已经标记为没有库存,则直接返回,无需进行Redis预减 if (entryStockMap.get(goodsId)) { model.addAttribute("errmsg", RespBeanEnum.ENTRY_STOCK.getMessage()); return "secKillFail";//错误页面 } // 1. 获取锁,setnx // 得到一个 uuid 值,作为锁的值 String uuid = UUID.randomUUID().toString(); // 锁放入到 Redis 当中,过期时间 3 秒 Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 3, TimeUnit.SECONDS); // 1.1 定义 lua脚本 String script = "if redis.call('get', KEYS[1]) == ARGV[1] " + "then return redis.call('del', KEYS[1]) else return 0 end"; // 使用 redis 执行 lua 执行 DefaultRedisScript redisScript = new DefaultRedisScript<>(); redisScript.setScriptText(script); redisScript.setResultType(Long.class); // 2. 获取锁成功,查询 num 的值 if (lock) { // 执行你自己的业务-这里就可以有多个操作了,都具有了原子性 // Redis库存预减,如果在 Redis 中预减库存,发现秒杀商品已经没有了,就直接返回 // 从面减少去执行 orderService.seckill()请求,防止线程堆积,优化秒杀/高并发 // 提示: Redis 的 decrement是具有原子性的,已经存在了原子性,就是一条一条执行的,不会存在,复购,多购的可能性。 // 注意:这里我们要操作的 key 的是:seckillGoods:商品Id,value:该商品的库存量 Long decrement = redisTemplate.opsForValue().decrement("seckillGoods:" + goodsId); if (decrement < 0) { // 说明这个商品已经没有库存了,返回 // 说明当前秒杀的商品,已经没有库存 entryStockMap.put(goodsId, true); // 这里我们可以恢复库存为 0 ,因为后面可能会一直减下去,恢复为 0 让数据更好看一些 redisTemplate.opsForValue().increment("seckillGoods:" + goodsId); // 释放分布式锁,lua 为什么使用 redis+lua脚本释放锁,前面说过在 Redis 内容当中 redisTemplate.execute(redisScript, Arrays.asList("lock"), uuid); model.addAttribute("errmsg", RespBeanEnum.REPEAT_ERROR.getMessage()); // 将错误信息返回给下一页的模板当中 return "secKillFail"; // 返回一个错误页面 } // 释放分布式锁,lua 为什么使用 redis+lua脚本释放锁,前面说过在 Redis 内容当中 redisTemplate.execute(redisScript, Arrays.asList("lock"), uuid); System.out.println("秒杀 V 7.0 "); } else { // 3. 获取锁失败 model.addAttribute("errmsg", RespBeanEnum.SET_KILL_RETRY.getMessage()); return "secKillFail"; } //抢购,向消息队列发送秒杀请求,实现了秒杀异步请求 //这里我们发送秒杀消息后,立即快速返回结果[临时结果] - "比如排队中.." //客户端可以通过轮询,获取到最终结果 //创建SeckillMessage //SeckillMessage seckillMessage = new SeckillMessage(user, goodsId); SeckillMessage seckillMessage = new SeckillMessage(user, goodsId); mqSenderMessage.sendSeckillMessage(JSONUtil.toJsonStr(seckillMessage)); model.addAttribute("errmsg", "排队中..."); return "secKillFail"; }
复制代码 测试:和上述一样使用 Jmeter 进行一个压测。
优化:这里我们将 Redis 释放锁的 Lua 脚本专门放到 resources 目录下,创建 lock.lua脚本。 增加配置执行脚本
- 在\resources\目录下创建 lock.lua脚本。
- if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end
复制代码
- RedisConfig.java, 增加配置执行脚本
- package com.rainbowsea.seckill.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ClassPathResource; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; /** * 把session信息提取出来存到redis中 * 主要实现序列化, 这里是以常规操作 * @author Rainbowsea * @version 1.0 */ @Configuration public class RedisConfig { /** * 增加执行脚本 * @return DefaultRedisScript */ @Bean public DefaultRedisScript script() { DefaultRedisScript redisScript = new DefaultRedisScript<>(); //设置要执行的lua脚本位置, 把lock.lua文件放在resources redisScript.setLocation(new ClassPathResource("lock.lua")); redisScript.setResultType(Long.class); return redisScript; } }
复制代码- package com.rainbowsea.seckill.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ClassPathResource; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; /** * 把session信息提取出来存到redis中 * 主要实现序列化, 这里是以常规操作 * @author Rainbowsea * @version 1.0 */ @Configuration public class RedisConfig { /** * 自定义 RedisTemplate对象, 注入到容器 * 后面我们操作Redis时,就使用自定义的 RedisTemplate对象 * @param redisConnectionFactory * @return RedisTemplate */ @Bean public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate redisTemplate = new RedisTemplate<>(); //设置相应key的序列化 redisTemplate.setKeySerializer(new StringRedisSerializer()); //value序列化 //redis默认是jdk的序列化是二进制,这里使用的是通用的json数据,不用传具体的序列化的对象 redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); //设置相应的hash序列化 redisTemplate.setHashKeySerializer(new StringRedisSerializer()); redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); //注入连接工厂 redisTemplate.setConnectionFactory(redisConnectionFactory); System.out.println("测试--> redisTemplate" + redisTemplate.hashCode()); return redisTemplate; } /** * 增加执行脚本 * @return DefaultRedisScript */ @Bean public DefaultRedisScript script() { DefaultRedisScript redisScript = new DefaultRedisScript<>(); //设置要执行的lua脚本位置, 把lock.lua文件放在resources redisScript.setLocation(new ClassPathResource("lock.lua")); redisScript.setResultType(Long.class); return redisScript; } }
复制代码 修改:SckillController 控制层当中的 秒杀代码处理位置。
- @Resource private RedisScript script; @RequestMapping(value = "/doSeckill") public String doSeckill(Model model, User user, Long goodsId) { System.out.println("秒杀 V 8.0 "); if (user == null) {//用户没有登录 return "login"; } //将user放入到model, 下一个模板可以使用 model.addAttribute("user", user); //获取到goodsVo GoodsVo goodsVo = goodsService.findGoodsVoByGoodsId(goodsId); //判断库存 if (goodsVo.getStockCount() < 1) {//没有库存 model.addAttribute("errmsg", RespBeanEnum.ENTRY_STOCK.getMessage()); return "secKillFail";//错误页面 } //判断用户是否复购-直接到Redis中,获取对应的秒杀订单,如果有,则说明已经抢购了 SeckillOrder o = (SeckillOrder) redisTemplate.opsForValue() .get("order:" + user.getId() + ":" + goodsVo.getId()); if (null != o) { //说明该用户已经抢购了该商品 model.addAttribute("errmsg", RespBeanEnum.REPEAT_ERROR.getMessage()); return "secKillFail";//错误页面 } //对map进行判断[内存标记],如果商品在map已经标记为没有库存,则直接返回,无需进行Redis预减 if (entryStockMap.get(goodsId)) { model.addAttribute("errmsg", RespBeanEnum.ENTRY_STOCK.getMessage()); return "secKillFail";//错误页面 } // 1. 获取锁,setnx // 得到一个 uuid 值,作为锁的值 String uuid = UUID.randomUUID().toString(); // 锁放入到 Redis 当中,过期时间 3 秒 Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 3, TimeUnit.SECONDS); // lua 脚本,从Spring IOC 容器当中获取了。为 script,如下代码获取到了 /* @Resource private RedisScript script; */ // 2. 获取锁成功,查询 num 的值 if (lock) { // 执行你自己的业务-这里就可以有多个操作了,都具有了原子性 // Redis库存预减,如果在 Redis 中预减库存,发现秒杀商品已经没有了,就直接返回 // 从面减少去执行 orderService.seckill()请求,防止线程堆积,优化秒杀/高并发 // 提示: Redis 的 decrement是具有原子性的,已经存在了原子性,就是一条一条执行的,不会存在,复购,多购的可能性。 // 注意:这里我们要操作的 key 的是:seckillGoods:商品Id,value:该商品的库存量 Long decrement = redisTemplate.opsForValue().decrement("seckillGoods:" + goodsId); if (decrement < 0) { // 说明这个商品已经没有库存了,返回 // 说明当前秒杀的商品,已经没有库存 entryStockMap.put(goodsId, true); // 这里我们可以恢复库存为 0 ,因为后面可能会一直减下去,恢复为 0 让数据更好看一些 redisTemplate.opsForValue().increment("seckillGoods:" + goodsId); // 释放分布式锁,lua 为什么使用 redis+lua脚本释放锁,前面说过在 Redis 内容当中 redisTemplate.execute(script, Arrays.asList("lock"), uuid); model.addAttribute("errmsg", RespBeanEnum.REPEAT_ERROR.getMessage()); // 将错误信息返回给下一页的模板当中 return "secKillFail"; // 返回一个错误页面 } // 释放分布式锁,lua 为什么使用 redis+lua脚本释放锁,前面说过在 Redis 内容当中 redisTemplate.execute(script, Arrays.asList("lock"), uuid); System.out.println("秒杀 V 8.0 "); } else { // 3. 获取锁失败 model.addAttribute("errmsg", RespBeanEnum.SET_KILL_RETRY.getMessage()); return "secKillFail"; } //抢购,向消息队列发送秒杀请求,实现了秒杀异步请求 //这里我们发送秒杀消息后,立即快速返回结果[临时结果] - "比如排队中.." //客户端可以通过轮询,获取到最终结果 //创建SeckillMessage //SeckillMessage seckillMessage = new SeckillMessage(user, goodsId); SeckillMessage seckillMessage = new SeckillMessage(user, goodsId); mqSenderMessage.sendSeckillMessage(JSONUtil.toJsonStr(seckillMessage)); model.addAttribute("errmsg", "排队中..."); return "secKillFail"; }
复制代码- package com.rainbowsea.seckill.controller; import cn.hutool.json.JSONUtil; import com.rainbowsea.seckill.config.AccessLimit; import com.rainbowsea.seckill.pojo.SeckillMessage; import com.rainbowsea.seckill.pojo.SeckillOrder; import com.rainbowsea.seckill.pojo.User; import com.rainbowsea.seckill.rabbitmq.MQSenderMessage; import com.rainbowsea.seckill.service.GoodsService; import com.rainbowsea.seckill.service.OrderService; import com.rainbowsea.seckill.service.SeckillOrderService; import com.rainbowsea.seckill.vo.GoodsVo; import com.rainbowsea.seckill.vo.RespBean; import com.rainbowsea.seckill.vo.RespBeanEnum; import com.ramostear.captcha.HappyCaptcha; import com.ramostear.captcha.common.Fonts; import com.ramostear.captcha.support.CaptchaStyle; import com.ramostear.captcha.support.CaptchaType; import org.springframework.beans.factory.InitializingBean; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ValueOperations; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.data.redis.core.script.RedisScript; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.UUID; import java.util.concurrent.TimeUnit; @Controller @RequestMapping("/seckill") // InitializingBean 当中的 afterPropertiesSet 表示项目启动就自动给执行该方法当中的内容 public class SeckillController implements InitializingBean { // 装配需要的组件/对象 @Resource private GoodsService goodsService; @Resource private SeckillOrderService seckillOrderService; @Resource private OrderService orderService; // 如果某个商品库存已经为空, 则标记到 entryStockMap @Resource private RedisTemplate redisTemplate; // 定义 map- 记录秒杀商品 private HashMap entryStockMap = new HashMap<>(); // 装配消息的生产者/发送者 @Resource private MQSenderMessage mqSenderMessage; /** * 方法: 处理用户抢购请求/秒杀 * 说明: 我们先完成一个 V 8.0版本, * - 利用 MySQL默认的事务隔离级别【REPEATABLE-READ】 * - 使用 优化秒杀: Redis 预减库存+Decrement * - 优化秒杀: 加入内存标记,避免总到 Redis 查询库存 * - 优化秒杀: 加入消息队列,实现秒杀的异步请求 * - 优化: 扩展: 采用 Redis 分布式锁,控制事务 * - 优化:使用增加配置执行脚本,执行 lua 脚本 * * @param model 返回给模块的 model 信息 * @param user User 通过用户使用了,自定义参数解析器获取 User 对象, * @param goodsId 秒杀商品的 ID 信息 * @return 返回到映射在 resources 下的 templates 下的页面 */ @Resource private RedisScript script; @RequestMapping(value = "/doSeckill") public String doSeckill(Model model, User user, Long goodsId) { System.out.println("秒杀 V 8.0 "); if (user == null) {//用户没有登录 return "login"; } //将user放入到model, 下一个模板可以使用 model.addAttribute("user", user); //获取到goodsVo GoodsVo goodsVo = goodsService.findGoodsVoByGoodsId(goodsId); //判断库存 if (goodsVo.getStockCount() < 1) {//没有库存 model.addAttribute("errmsg", RespBeanEnum.ENTRY_STOCK.getMessage()); return "secKillFail";//错误页面 } //判断用户是否复购-直接到Redis中,获取对应的秒杀订单,如果有,则说明已经抢购了 SeckillOrder o = (SeckillOrder) redisTemplate.opsForValue() .get("order:" + user.getId() + ":" + goodsVo.getId()); if (null != o) { //说明该用户已经抢购了该商品 model.addAttribute("errmsg", RespBeanEnum.REPEAT_ERROR.getMessage()); return "secKillFail";//错误页面 } //对map进行判断[内存标记],如果商品在map已经标记为没有库存,则直接返回,无需进行Redis预减 if (entryStockMap.get(goodsId)) { model.addAttribute("errmsg", RespBeanEnum.ENTRY_STOCK.getMessage()); return "secKillFail";//错误页面 } // 1. 获取锁,setnx // 得到一个 uuid 值,作为锁的值 String uuid = UUID.randomUUID().toString(); // 锁放入到 Redis 当中,过期时间 3 秒 Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 3, TimeUnit.SECONDS); // lua 脚本,从Spring IOC 容器当中获取了。为 script,如下代码获取到了 /* @Resource private RedisScript script; */ // 2. 获取锁成功,查询 num 的值 if (lock) { // 执行你自己的业务-这里就可以有多个操作了,都具有了原子性 // Redis库存预减,如果在 Redis 中预减库存,发现秒杀商品已经没有了,就直接返回 // 从面减少去执行 orderService.seckill()请求,防止线程堆积,优化秒杀/高并发 // 提示: Redis 的 decrement是具有原子性的,已经存在了原子性,就是一条一条执行的,不会存在,复购,多购的可能性。 // 注意:这里我们要操作的 key 的是:seckillGoods:商品Id,value:该商品的库存量 Long decrement = redisTemplate.opsForValue().decrement("seckillGoods:" + goodsId); if (decrement < 0) { // 说明这个商品已经没有库存了,返回 // 说明当前秒杀的商品,已经没有库存 entryStockMap.put(goodsId, true); // 这里我们可以恢复库存为 0 ,因为后面可能会一直减下去,恢复为 0 让数据更好看一些 redisTemplate.opsForValue().increment("seckillGoods:" + goodsId); // 释放分布式锁,lua 为什么使用 redis+lua脚本释放锁,前面说过在 Redis 内容当中 redisTemplate.execute(script, Arrays.asList("lock"), uuid); model.addAttribute("errmsg", RespBeanEnum.REPEAT_ERROR.getMessage()); // 将错误信息返回给下一页的模板当中 return "secKillFail"; // 返回一个错误页面 } // 释放分布式锁,lua 为什么使用 redis+lua脚本释放锁,前面说过在 Redis 内容当中 redisTemplate.execute(script, Arrays.asList("lock"), uuid); System.out.println("秒杀 V 8.0 "); } else { // 3. 获取锁失败 model.addAttribute("errmsg", RespBeanEnum.SET_KILL_RETRY.getMessage()); return "secKillFail"; } //抢购,向消息队列发送秒杀请求,实现了秒杀异步请求 //这里我们发送秒杀消息后,立即快速返回结果[临时结果] - "比如排队中.." //客户端可以通过轮询,获取到最终结果 //创建SeckillMessage //SeckillMessage seckillMessage = new SeckillMessage(user, goodsId); SeckillMessage seckillMessage = new SeckillMessage(user, goodsId); mqSenderMessage.sendSeckillMessage(JSONUtil.toJsonStr(seckillMessage)); model.addAttribute("errmsg", "排队中..."); return "secKillFail"; } /** * 获取秒杀路径 * * @param user 用户信息 * @param goodsId 秒杀商品ID * @return RespBean 返回信息,携带秒杀路径 path * -v 4.0 增加了 happyCaptcha 验证码 * - 增加 Redis 计数器,完成对用户的限流防刷 * - 通用接口限流-防刷-封装为-一个注解搞定 * second = 5, maxCount = 5 说明是在 5 秒内可以访问的最大次数是 5 次 * needLogin = true 表示用户是否需要登录,true 表示用户需要登录 */ @RequestMapping("/path") @ResponseBody @AccessLimit(second = 5, maxCount = 5, needLogin = true) public RespBean getPath(User user, Long goodsId, String captcha, HttpServletRequest request) { // 我们的设计的商品 gooodsId 是一定大于 0 的 if (user == null || goodsId < 0 || !StringUtils.hasText(captcha)) { return RespBean.error(RespBeanEnum.SESSION_ERROR); } // 增加一个业务逻辑-校验用户输入的验证码是否正确 boolean check = orderService.checkCaptcha(user, goodsId, captcha); if (!check) { return RespBean.error(RespBeanEnum.CAPTCHA_ERROR); } String path = orderService.createPath(user, goodsId); return RespBean.success(path); } /** * InitializingBean 接口当中的 afterPropertiesSet 表示项目启动就自动给执行该方法当中的内容 * 该方法是在类的所有属性,都是初始化后,自动执行的 * 这里我们就可以将所有秒杀商品的库存量,加载到 Redis 当中 * * @throws Exception */ @Override public void afterPropertiesSet() throws Exception { // 获取所有可以秒杀的商品信息 List list = goodsService.findGoodsVo(); // 先判断是否为空 if (CollectionUtils.isEmpty(list)) { return; } // 遍历 List,然后将秒杀商品的库存量,放入到 Redis // key:秒杀商品库存量对应 key:seckillGoods:商品Id,value:该商品的库存量 list.forEach( goodsVo -> { redisTemplate.opsForValue() .set("seckillGoods:" + goodsVo.getId(), goodsVo.getStockCount()); // 初始化 map // 如果 goodsId: false 表示有库存 // 如果 goodsId: true 表示没有库存 entryStockMap.put(goodsVo.getId(), false); }); } /** * 生成验证码 * 注意:HappyCaptcha 执行该方法后,会自动默认将验证码放入到 Session 当中。对应 HappyCaptcha验证码的Key为“happy-captcha”。 * 手动清理Session中存放的验证码,HappyCaptcha验证码的Key为“happy-captcha”。 * 这里我们考虑到项目的分布式,如果将验证码存入到 Session 当中,如果采用分布式,不同机器可能 * 登录访问的该验证码就不存在,不同的机器当中,就像我们上面设置的共享 Session 的问题是一样的 * 所以这里我们同时也将 HappyCaptcha验证码的存储到 Redis 当中。Redis 当中验证码的key设计为:captcha:userId:goodsId * 同时设置超时时间 100s,过后没登录就,该验证码失效 * * @param request * @param response */ @GetMapping("/captcha") public void captcha(User user, Long goodsId , HttpServletRequest request, HttpServletResponse response) { HappyCaptcha.require(request, response) .style(CaptchaStyle.IMG) //设置展现样式为图片 .type(CaptchaType.NUMBER) //设置验证码内容为数字 .length(5) //设置字符长度为5 .width(220) //设置动画宽度为220 .height(80) //设置动画高度为80 .font(Fonts.getInstance().zhFont()) //设置汉字的字体 .build().finish(); //生成并输出验证码 // 从 Session 当中把验证码的值,保存 Redis当中【考虑项目分布式】,同时设计验证码 100s 失效 // Redis 当中验证码的key设计为:captcha:userId:goodsId redisTemplate.opsForValue().set("captcha:" + user.getId() + ":" + goodsId, (String) request.getSession().getAttribute("happy-captcha"), 100, TimeUnit.SECONDS); } }
复制代码 测试:
- 确保多用户的 userticket 已经正确的保存到 Redis 中
- 确保商品库存已经正确的加载/保存到 Redis 中, 并且没有订单生成
- 启动线程组,进行测试
- 测试结果, 不在出现超卖和复购问题
用户名是手机号: 13300000000 密码为: 123456 加密
项目启动准备说明
- 用户名是手机号: 13300000000 密码为: 123456 加密
- 启动对应的 Redis ,同时修改对应配置,密码,特别是对应的变化的 IP 地址。注意:查看 Redis 是否真正启动了。
- 启动修改 MySQL 数据库的地址,以及密码
- 启动修改对应的 RabbitMQ 消息队列的,特别是对应的变化的 IP 地址,以及对应的账户和密码。注意:查看 RabbitMQ 是否真正启动了。
- 对应的数据库是: seckill 我这里。
- 注意:这里 Jmeter 压测,需要重置数据表,同时生成 2000 用户是在: com.rainbowsea.seckill.utill.UserUtil包的这个 UserUtil 类。
最后:
“在这个最后的篇章中,我要表达我对每一位读者的感激之情。你们的关注和回复是我创作的动力源泉,我从你们身上吸取了无尽的灵感与勇气。我会将你们的鼓励留在心底,继续在其他的领域奋斗。感谢你们,我们总会在某个时刻再次相遇。”
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |