秒杀因其流量大,并发高,成为最经典的业务之一,并发的根源在于数据库,通常采用缓存+队列的方式减少数据库访问,降低访问速度。有幸接触到一个秒杀业务代码,采用redis+mq的方式实现,现整理流程如下:
1. 数据库设计
秒杀业务的商品和订单单独设表,不要和正常商品和商品订单混合一起。表格数据足够少,才能保证查询和写入速度。订单表将用户id和商品id设置为唯一索引,这样能保证同一个用户不会购买两件相同的秒杀商品。
秒杀商品表商品库存更新语句添加>0条件,保证商品卖超的情况。
update miaosha_goods set stock_count = stock_count - 1 where goods_id = #{goodsId} and stock_count > 0
2. 使用redis建立分布式session机制
用户登录生成token,保存到redis中,key值使用前缀+token,value保存秒杀用户信息,将token封装进cookie中进行返回。
3. 以拦截器方式验证所有接口用户信息是否合法,并将用户信息保存至上下文中
首先通过容器方式对WebMvcConfigurerAdapter添加拦截器,并为方法添加参数;
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter{
@Autowired
UserArgumentResolver userArgumentResolver;
@Autowired
AccessInterceptor accessInterceptor;
//为方法添加秒杀用户参数
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(userArgumentResolver);
}
//使用拦截器将用户信息保存至上下文中
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(accessInterceptor);
}
}
@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();
}
}
AccessInterceptor继承HandlerInterceptorAdapter,实现 preHandle方法,根据token将用户信息从redis取出放入上下文中,上下文类使用ThreadLocal保存:
private static ThreadLocal<MiaoshaUser> userHolder = new ThreadLocal<MiaoshaUser>();
4. 秒杀安全防护:防刷限流
1)秒杀开始前使用公式验证码进行验证,可用技术:scriptEngine
2)隐藏秒杀接口,动态获取秒杀路径:路径可以使用localhost:8081/miaosha/{path}/doMiaosha的方式,{path}则通过动态方式生成,生成逻辑:UUID+盐值 然后使用MD5加密,同时保存至redis中一份:key使用用户id+商品id。
3)添加访问限制注解,为获取动态秒杀接口添加访问限制,例如:3分钟内限制访问5次。
@Retention(RUNTIME)
@Target(METHOD)
public @interface AccessLimit {
int seconds();
int maxCount();
boolean needLogin() default true;
}
5. 实现秒杀业务逻辑:
(1)判断用户是否存在,不存在则报session异常
(2)判断路径是否存在,从redis中根据用户id和商品id查询路径,不存在则报请求非法
(3)使用系统内存,减少redis访问,从系统内存中根据商品id获得是否还存在商品,true或者false,不存在则返回商品无剩余
private HashMap<Long, Boolean> localOverMap = new HashMap<Long, Boolean>();
(4)redis预减库存,如果库存减少为0,则更新本地内存,将id对应的商品设置为over
(5)查询redis是否已存在订单,存在则返回重复秒杀
(6)秒杀入队(mq)
(7)出队:
a) 判断秒杀货物是否有剩余,无剩余则 return;
b) 判断是否已经存在订单,存在则return;
c) 减库存,下订单,保存秒杀订单
总结
秒杀业务的根本在于应用缓存提高并发量,减少数据库访问,使用mq缓冲将大流量数据进行削峰填谷,另外注意接口的防刷限流。
代码详见:https://download.csdn.net/download/u012466304/10751632