赞
踩
/** * 系统初始化,将秒杀商品库存加载到redis中 * * @throws Exception */ @Override public void afterPropertiesSet() throws Exception { // 将秒杀商品库存加载到redis中 List<GoodsVo> goodsVoList = goodsService.findGoodsVo(); // 如果没有秒杀商品,直接返回 if (CollectionUtils.isEmpty(goodsVoList)) { return; } goodsVoList.forEach(goodsVo -> { redisTemplate.opsForValue().set("seckillGoods:" + goodsVo.getId(), goodsVo.getStockCount()); }); }
// 库存预减
Long stock = redisTemplate.opsForValue().decrement("seckillGoods:" + goodsId);
// 判断库存是否充足
if (stock < 0) {
// 库存不足,返回秒杀失败页面
redisTemplate.opsForValue().increment("seckillGoods:" + goodsId);
model.addAttribute("errmsg", RespBeanEnum.EMPTY_STOCK.getMessage());
return "secKillFail";
}
package com.sxs.seckill.pojo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; /** * Description: 秒杀消息 * * @Author sun * @Create 2024/5/13 14:15 * @Version 1.0 */ @Data @NoArgsConstructor @AllArgsConstructor public class SeckillMessage { private User user; private Long goodsId; }
package com.sxs.seckill.config; import org.springframework.amqp.core.Binding; import org.springframework.amqp.core.BindingBuilder; import org.springframework.amqp.core.Queue; import org.springframework.amqp.core.TopicExchange; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * Description: 秒杀RabbitMQ配置 * * @Author sun * @Create 2024/5/13 14:23 * @Version 1.0 */ @Configuration public class RabbitMQSeckillConfig { // 定义一个消息队列和一个topic交换机的名字 public static final String SECKILL_QUEUE = "seckillQueue"; public static final String SECKILL_EXCHANGE = "seckillExchange"; // 创建一个消息队列 @Bean public Queue seckillQueue() { return new Queue(SECKILL_QUEUE, true); } // 创建一个topic交换机 @Bean public TopicExchange seckillExchange() { return new TopicExchange(SECKILL_EXCHANGE); } // 将消息队列绑定到交换机 @Bean public Binding binding() { // 绑定消息队列到交换机,并指定routingKey,表示只接收routingKey为seckill.#的消息 return BindingBuilder.bind(seckillQueue()).to(seckillExchange()).with("seckill.#"); } }
package com.sxs.seckill.rabbitmq; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.stereotype.Service; import javax.annotation.Resource; /** * Description: 消息队列发送消息 * * @Author sun * @Create 2024/5/13 15:14 * @Version 1.0 */ @Service @Slf4j public class MQSendMessage { @Resource private RabbitTemplate rabbitTemplate; // 发送秒杀消息 public void sendSeckillMessage(String message) { log.info("发送消息:" + message); rabbitTemplate.convertAndSend("seckillExchange", "seckill.message", message); } }
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.3.3</version>
</dependency>
package com.sxs.seckill.rabbitmq; import cn.hutool.json.JSONUtil; import com.sxs.seckill.pojo.SeckillMessage; import com.sxs.seckill.pojo.User; import com.sxs.seckill.service.GoodsService; import com.sxs.seckill.service.OrderService; import com.sxs.seckill.service.SeckillGoodsService; import com.sxs.seckill.vo.GoodsVo; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Service; import javax.annotation.Resource; /** * Description: 消息队列接收消息 * * @Author sun * @Create 2024/5/13 15:17 * @Version 1.0 */ @Service @Slf4j public class MQReceiverMessage { @Resource private GoodsService goodsService; @Resource private OrderService orderService; // 接收秒杀消息 @RabbitListener(queues = "seckillQueue") public void receiveSeckillMessage(String message) { log.info("接收消息:" + message); // 此时的message是秒杀的消息,要将其转换为SeckillMessage对象 SeckillMessage seckillMessage = JSONUtil.toBean(message, SeckillMessage.class); // 获取秒杀信息 User user = seckillMessage.getUser(); Long goodsId = seckillMessage.getGoodsId(); // 根据商品id查询商品详情 GoodsVo goodsVoByGoodsId = goodsService.findGoodsVoByGoodsId(goodsId); // 进行秒杀 orderService.seckill(user, goodsVoByGoodsId); } }
// MQ实现异步秒杀
// 封装秒杀信息
SeckillMessage seckillMessage = new SeckillMessage(user, goodsId);
// 使用hutool工具类将SeckillMessage对象转换为json字符串并发送
mqSendMessage.sendSeckillMessage(JSONUtil.toJsonStr(seckillMessage));
// 返回排队中页面
model.addAttribute("errmsg", RespBeanEnum.QUEUE_ERROR.getMessage());
return "secKillFail";
/** * 方法:生成秒杀路径 * @param user * @param goodsId * @return */ String createPath(User user, Long goodsId); /** * 方法:校验秒杀路径 * @param user * @param goodsId * @param path * @return */ boolean checkPath(User user, Long goodsId, String path);
@Override public String createPath(User user, Long goodsId) { // 对参数进行校验 if (user == null || goodsId <= 0) { return null; } // 生成秒杀路径 String path = MD5Util.md5(UUIDUtil.uuid() + "123456"); // 保存到redis中,设置过期时间为60秒 redisTemplate.opsForValue().set("seckillPath:" + user.getId() + ":" + goodsId, path, 60, TimeUnit.SECONDS); return path; } @Override public boolean checkPath(User user, Long goodsId, String path) { // 对参数进行校验 if (user == null || goodsId <= 0 || StringUtils.isBlank(path)) { return false; } // 从redis中获取秒杀路径 String redisPath = (String) redisTemplate.opsForValue().get("seckillPath:" + user.getId() + ":" + goodsId); // 判断是否相等,并返回 return path.equals(redisPath); }
@RequestMapping("/{path}/doSeckill") public RespBean doSeckill(Model model, User user, Long goodsId, @PathVariable String path) { // 判断用户是否登录 if (user == null) { return RespBean.error(RespBeanEnum.SESSION_ERROR); } // 校验path boolean check = orderService.checkPath(user, goodsId, path); if (!check) { return RespBean.error(RespBeanEnum.REQUEST_ILLEGAL); } // 根据goodsId获取GoodsVo GoodsVo goodsVoByGoodsId = goodsService.findGoodsVoByGoodsId(goodsId); // 判断是否有库存 if (goodsVoByGoodsId.getStockCount() < 1) { return RespBean.error(RespBeanEnum.EMPTY_STOCK); } // 从redis中判断是否复购 if (redisTemplate.hasKey("order:" + user.getId() + ":" + goodsId)) { return RespBean.error(RespBeanEnum.REPEATE_ERROR); } // 首先判断内存标记 if (inventoryTagging.get(goodsId)) { return RespBean.error(RespBeanEnum.EMPTY_STOCK); } // 库存预减 Long stock = redisTemplate.opsForValue().decrement("seckillGoods:" + goodsId); // 判断库存是否充足 if (stock < 0) { // 标记库存不足 inventoryTagging.put(goodsId, true); // 库存不足,返回秒杀失败页面 redisTemplate.opsForValue().increment("seckillGoods:" + goodsId); return RespBean.error(RespBeanEnum.EMPTY_STOCK); } // MQ实现异步秒杀 // 封装秒杀信息 SeckillMessage seckillMessage = new SeckillMessage(user, goodsId); // 使用hutool工具类将SeckillMessage对象转换为json字符串并发送 mqSendMessage.sendSeckillMessage(JSONUtil.toJsonStr(seckillMessage)); // 返回排队中 return RespBean.success(RespBeanEnum.SEK_KILL_WAIT); } /** * 生成秒杀地址 * @param user * @param goodsId * @return */ @ResponseBody @RequestMapping("/path") public RespBean getPath(User user, Long goodsId) { // 参数校验 if (user == null || goodsId <= 0) { return RespBean.error(RespBeanEnum.REQUEST_ILLEGAL); } // 调用OrderService中的createPath方法生成秒杀地址 String path = orderService.createPath(user, goodsId); return RespBean.success(path); }
<dependency>
<groupId>com.ramostear</groupId>
<artifactId>Happy-Captcha</artifactId>
<version>1.0.1</version>
</dependency>
/** * 生成验证码 * @param user * @param goodsId * @param request * @param response */ @RequestMapping("/captcha") public void happyCaptcha(User user, Long goodsId, HttpServletRequest request, HttpServletResponse response) { HappyCaptcha.require(request, response) .style(CaptchaStyle.ANIM) //设置展现样式为动画 .type(CaptchaType.NUMBER) //设置验证码内容为数字 .length(6) //设置字符长度为 6 .width(220) //设置动画宽度为 220 .height(80) //设置动画高度为 80 .font(Fonts.getInstance().zhFont()) //设置汉字的字体 .build().finish(); //生成并输出验证码 // 这个验证码的结果会存储在session中,可以通过request.getSession().getAttribute("happy-captcha")获取 // 获取验证码的值,放入redis中 String verifyCode = request.getSession().getAttribute("happy-captcha").toString(); redisTemplate.opsForValue().set("captcha:" + user.getId() + ":" + goodsId, verifyCode, 60, TimeUnit.SECONDS); }
/**
* 校验用户输入的验证码
* @param user
* @param goodsId
* @param captcha
* @return
*/
boolean checkCaptcha(User user, Long goodsId, String captcha);
@Override
public boolean checkCaptcha(User user, Long goodsId, String captcha) {
// 参数校验
if (user == null || goodsId <= 0 || StringUtils.isBlank(captcha)) {
return false;
}
// 从redis中获取验证码
String verifyCode = (String) redisTemplate.opsForValue().get("captcha:" + user.getId() + ":" + goodsId);
return captcha.equals(verifyCode);
}
package com.sxs.seckill.config; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * Description: 限流注解 * * @Author sun * @Create 2024/5/14 15:38 * @Version 1.0 */ @Retention(RetentionPolicy.RUNTIME) // 运行时生效 @Target(ElementType.METHOD) // 作用在方法上 public @interface AccessLimit { int seconds(); // 时间范围 int maxCount(); // 最大访问次数 boolean needLogin() default true; // 是否需要登录 }
package com.sxs.seckill.config; import com.sxs.seckill.pojo.User; /** * Description: * * @Author sun * @Create 2024/5/14 15:46 * @Version 1.0 */ public class UserContext { // 初始化ThreadLocal以存储用户信息 private static ThreadLocal<User> threadLocal = new ThreadLocal<>(); public static User getUser() { return threadLocal.get(); } public static void setUser(User user) { threadLocal.set(user); } // 清除ThreadLocal中的数据 public static void removeUser() { threadLocal.remove(); } }
package com.sxs.seckill.config; import com.sxs.seckill.exception.GlobalException; import com.sxs.seckill.pojo.User; import com.sxs.seckill.service.UserService; import com.sxs.seckill.utils.CookieUtil; import com.sxs.seckill.vo.RespBeanEnum; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.concurrent.TimeUnit; /** * Description: 限流拦截器 * * @Author sun * @Create 2024/5/14 15:55 * @Version 1.0 */ @Component public class AccessLimitInterceptor implements HandlerInterceptor { @Resource private UserService userService; @Resource RedisTemplate redisTemplate; /** * 拦截请求,进行限流处理,在目标方法前执行 * * @param request * @param response * @param handler * @return * @throws Exception */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (handler instanceof HandlerMethod) { // 如果是方法级别的拦截 // 1.获取user对象,放到threadLocal中 User user = getUser(request, response); UserContext.setUser(user); // 2.处理限流注解 HandlerMethod handlerMethod = (HandlerMethod) handler; AccessLimit accessLimit = handlerMethod.getMethodAnnotation(AccessLimit.class); if (accessLimit == null) { return true; } // 3.获取注解上的参数 int seconds = accessLimit.seconds(); int maxCount = accessLimit.maxCount(); boolean needLogin = accessLimit.needLogin(); String key = request.getRequestURI(); if (needLogin) { // 如果需要登录,但是没有登录,返回错误信息 if (user == null) { // 如果需要登录,但是没有登录,返回错误信息 throw new GlobalException(RespBeanEnum.USER_NOT_LOGIN); } // 如果登录了,key加上用户id key += ":" + user.getId(); } // 4.对访问次数进行限制,如果登陆了就是对这个用户的访问次数进行限制,如果没有登录就是对这个接口的访问次数进行限制 Integer count = (Integer) redisTemplate.opsForValue().get(key); if (count == null) { // 第一次访问 redisTemplate.opsForValue().set(key, 1, seconds, TimeUnit.SECONDS); } else if (count < maxCount) { // 访问次数加1 redisTemplate.opsForValue().increment(key); } else { // 超过访问次数 throw new GlobalException(RespBeanEnum.ACCESS_LIMIT_REACHED); } } // 如果不是方法级别的拦截,直接放行 return true; } // 单独编写方法,获取User对象 private User getUser(HttpServletRequest request, HttpServletResponse response) { String ticket = CookieUtil.getCookieValue(request, "userTicket"); if (ticket == null) { return null; } return userService.getUserByCookie(ticket, request, response); } }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。