经常看到 某宝, 某东, 还有各种平台的秒杀 活动, 觉得很想学习一下秒杀技术,也顺便学习在 在高并发下系统的设计,于是学习了慕课网的秒杀教程。 这里写博客记录一下。
因为秒杀商品的经常变动所以设计了
CREATE TABLE `miaosha_goods` (
`id` bigint(20) NOT NULL,
`goods_id` bigint(20) NOT NULL,
`miaosha_price` decimal(10,2) NOT NULL,
`stock_count` int(11) NOT NULL,
`start_date` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP,
`end_date` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `miaosha_order` (
`id` bigint(20) NOT NULL,
`user_id` bigint(20) NOT NULL,
`order_id` bigint(20) NOT NULL,
`goods_id` bigint(20) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
这里不推荐后端模板引擎的原因是因为 模板需要后端渲染 , 生成页面,即使有缓存服务端的压力也过大。
主要手段:1 cdn , 2 nginx 的缓存,3 使用 压缩后的js ,4 开启 g-zip
通过 redis 缓存秒杀商品列表页, 和详情页的数据 ,查询出数据后交给前端模板引擎渲染
1 实现 InitializingBean 接口 重写 afterPropertiesSet 方法(在默认构造方法执行完之后执行)
/**
* 系统初始化
* */
@Override
public void afterPropertiesSet() throws Exception {
List goodsList = goodsService.listGoodsVo();
if(goodsList == null) {
return;
}
for(GoodsVo goods : goodsList) {
redisService.set(GoodsKey.getMiaoshaGoodsStock, ""+goods.getId(), goods.getStockCount());
// 如果是分布式系统 可以交给 redis 去做
localOverMap.put(goods.getId(), false);
}
}
1 构建秒杀消息并且通过 mq 发送, 然后同步返回 排队中
@Autowired
MQSender sender;
... 省略业务代码
MiaoshaMessage mm = new MiaoshaMessage();
mm.setUser(user);
mm.setGoodsId(goodsId);
sender.sendMiaoshaMessage(mm);
return Result.success(0);//排队中
1.1 发送者的简单实现
@Service
public class MQSender {
@Autowired
AmqpTemplate amqpTemplate ;
public void sendMiaoshaMessage(MiaoshaMessage mm) {
String msg = RedisService.beanToString(mm);
log.info("send message:"+msg);
amqpTemplate.convertAndSend(MQConfig.MIAOSHA_QUEUE, msg);
}
}
2 使用 定义 reciver 处理消息(监听指定队列, 接收消息)
@RabbitListener(queues=MQConfig.MIAOSHA_QUEUE)
public void receive(String message) {
log.info("receive message:"+message);
MiaoshaMessage mm = RedisService.stringToBean(message, MiaoshaMessage.class);
MiaoshaUser user = mm.getUser();
long goodsId = mm.getGoodsId();
GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId);
int stock = goods.getStockCount();
if(stock <= 0) {
return;
}
//判断是否已经秒杀到了
MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId);
if(order != null) {
return;
}
//减库存 下订单 写入秒杀订单
miaoshaService.miaosha(user, goods);
}
3 使用 事物 保证 秒杀操作的数据一致性
这里默认的隔离级别,使用了 行锁(独占锁) 保证库存不会卖超
@Transactional
public OrderInfo miaosha(MiaoshaUser user, GoodsVo goods) {
//减库存 下订单 写入秒杀订单,
boolean success = goodsService.reduceStock(goods);
if(success) {
//order_info maiosha_order
return orderService.createOrder(user, goods);
}else {
setGoodsOver(goods.getId());
return null;
}
}
4 客户端的处理
客户端 接收到秒杀接口的返回后,判断是否成功, 如果失败 直接提示给用户 秒杀失败 如果返回排队中, 则调用查询接口查询秒杀结果
public long getMiaoshaResult(Long userId, long goodsId) {
// 通过 redis 查询 该用户是否秒杀了指定产品
MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(userId, goodsId);
if(order != null) {//秒杀成功
return order.getOrderId();
}else {
// 获取该商品是否秒杀完的内存标记, 建议使用 redis
boolean isOver = getGoodsOver(goodsId);
if(isOver) {
return -1;
}else {
// 返回处理中 客户端隔一段时间以后继续发起查询
return 0;
}
}
}
通过验证码 可以有效分散用户请求,大大降低系统瞬间的并发, 大概思路就是 创建一个验证码图片,写给客户端,并且在服务端保存结果
@RequestMapping(value="/verifyCode", method=RequestMethod.GET)
@ResponseBody
public Result getMiaoshaVerifyCod(HttpServletResponse response,MiaoshaUser user,
@RequestParam("goodsId")long goodsId) {
if(user == null) {
return Result.error(CodeMsg.SESSION_ERROR);
}
try {
BufferedImage image = miaoshaService.createVerifyCode(user, goodsId);
OutputStream out = response.getOutputStream();
ImageIO.write(image, "JPEG", out);
out.flush();
out.close();
return null;
}catch(Exception e) {
e.printStackTrace();
return Result.error(CodeMsg.MIAOSHA_FAIL);
}
}
通过动态的秒杀地址,并且在商品开始秒杀之前 无法获取, 增加别人的破解难度
@AccessLimit(seconds=5, maxCount=5, needLogin=true)
@RequestMapping(value="/path", method=RequestMethod.GET)
@ResponseBody
public Result getMiaoshaPath(HttpServletRequest request, MiaoshaUser user,
@RequestParam("goodsId")long goodsId,
@RequestParam(value="verifyCode", defaultValue="0")int verifyCode
) {
if(user == null) {
return Result.error(CodeMsg.SESSION_ERROR);
}
boolean check = miaoshaService.checkVerifyCode(user, goodsId, verifyCode);
if(!check) {
return Result.error(CodeMsg.REQUEST_ILLEGAL);
}
String path =miaoshaService.createMiaoshaPath(user, goodsId);
return Result.success(path);
}
@Retention(RUNTIME)
@Target(METHOD)
public @interface AccessLimit {
int seconds();
int maxCount();
boolean needLogin() default true;
}
-2 通过 ThreadLocal 来保证每一个线程 持有一个秒杀对象
public class UserContext {
private static ThreadLocal userHolder = new ThreadLocal();
public static void setUser(MiaoshaUser user) {
userHolder.set(user);
}
public static MiaoshaUser getUser() {
return userHolder.get();
}
}
大概思路 通过方法拦截,获取方法上面的自定义注解, 然后 根据业务逻辑 自定义限流规则
@Service
public class AccessInterceptor extends HandlerInterceptorAdapter{
@Autowired
MiaoshaUserService userService;
@Autowired
RedisService redisService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
if(handler instanceof HandlerMethod) {
MiaoshaUser user = getUser(request, response);
UserContext.setUser(user);
HandlerMethod hm = (HandlerMethod)handler;
AccessLimit accessLimit = hm.getMethodAnnotation(AccessLimit.class);
if(accessLimit == null) {
return true;
}
int seconds = accessLimit.seconds();
int maxCount = accessLimit.maxCount();
boolean needLogin = accessLimit.needLogin();
String key = request.getRequestURI();
if(needLogin) {
if(user == null) {
render(response, CodeMsg.SESSION_ERROR);
return false;
}
key += "_" + user.getId();
}else {
//do nothing
}
AccessKey ak = AccessKey.withExpire(seconds);
Integer count = redisService.get(ak, key, Integer.class);
if(count == null) {
redisService.set(ak, key, 1);
}else if(count < maxCount) {
redisService.incr(ak, key);
}else {
render(response, CodeMsg.ACCESS_LIMIT_REACHED);
return false;
}
}
return true;
}
private void render(HttpServletResponse response, CodeMsg cm)throws Exception {
response.setContentType("application/json;charset=UTF-8");
OutputStream out = response.getOutputStream();
String str = JSON.toJSONString(Result.error(cm));
out.write(str.getBytes("UTF-8"));
out.flush();
out.close();
}
private MiaoshaUser getUser(HttpServletRequest request, HttpServletResponse response) {
String paramToken = request.getParameter(MiaoshaUserService.COOKI_NAME_TOKEN);
String cookieToken = getCookieValue(request, MiaoshaUserService.COOKI_NAME_TOKEN);
if(StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)) {
return null;
}
String token = StringUtils.isEmpty(paramToken)?cookieToken:paramToken;
return userService.getByToken(response, token);
}
private String getCookieValue(HttpServletRequest request, String cookiName) {
Cookie[] cookies = request.getCookies();
if(cookies == null || cookies.length <= 0){
return null;
}
for(Cookie cookie : cookies) {
if(cookie.getName().equals(cookiName)) {
return cookie.getValue();
}
}
return null;
}
}
@Service
public class UserArgumentResolver implements HandlerMethodArgumentResolver {
@Autowired
MiaoshaUserService userService;
public boolean supportsParameter(MethodParameter parameter) {
Class> clazz = parameter.getParameterType();
return clazz==MiaoshaUser.class;
}
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
return UserContext.getUser();
}
}
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter{
@Autowired
UserArgumentResolver userArgumentResolver;
@Autowired
AccessInterceptor accessInterceptor;
@Override
public void addArgumentResolvers(List argumentResolvers) {
argumentResolvers.add(userArgumentResolver);
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(accessInterceptor);
}
}
搞完收工,如果你喜欢博主的文章的话麻烦点一下关注,如果发现博主文章的错误的话 麻烦指出, 谢谢大家。