在开始设计秒杀模块的时候由于对一些知识了解还不够,设计的比较复杂,想把秒杀思路改变的心路历程都记录下来。
一、秒杀思路变形记
最初思路:
三层秒杀
1.在tomcat维护管理每个商品库存的线程,商品库存为0后撤销线程
2.在redis记录库存量和订单信息
3.在mysql记录库存量和订单信息
每次发起抢购请求,在redis写入订单信息并且在tomcat维护的库存线程上进行减一的原子操作
在后台开启一个线程,定期将原子计数器中的库存数和redis中的支付成功的订单信息加入到FIFO队列写入mysql
预计,
优点:
服务器响应速度快,只要redis写入成功且原子计数器减一即返回成功
缺点:
1.开发上稍麻烦
2.一旦tomcat上的线程阻塞或死亡用户将无法抢购
3.如果抢购的商品过多会维护过多的线程,占用系统资源导致崩溃
4.原子计数器是采用CAS操作,当多个的线程进行操作时失败操作会增多,而单线程操作处理速度又有局限性
5.组件之间的联系很重要,比如新库存数写入mysql和订单信息写入mysql本应是合并事务,如果其中一个线程出现问题都会导致数据不一致
后期思路:
两层秒杀
1.在redis记录库存量和订单信息
2.在mysql持久化库存量和订单信息
每次发起抢购请求,在redis中创建订单并令库存减一,支付成功后将订单信息、库存减一合并为事务操作后加入到FIFO队列
优点:
1.开发思路上组件清晰
2.redis是单线程,能保证操作原子性,且处理速度非常可观
缺点:
1.redis的连接量有限,每次在redis中的操作开启连接和关闭连接要造成许多开销
2.如果数据写入mysql失败,将会丢失
二、后台开发
不多说直接贴代码,注释的很清楚了,controller和InitFIFOListener的具体实现如下:
1.controller层代码
@RequestMapping("/createOrder.do")
@ResponseBody
public ServerResponse createOrder(Order order,HttpServletRequest request,HttpSession session) {
if (session.getAttribute("tel_num") == null || session.getAttribute("passwd") == null) {
return ServerResponse.createByErrorCodeMessage(ResponseCode.NEED_LOGIN.getCode(), "用户登录已过期");
}
//1.一个ip5分钟内只接受一次请求
//ps.暂未编码,编码基本思路是在Spring管理一个拦截器,重写preHandler接口
//由于会影响到测试效果,先注释掉
//2.判断商品是否存在
Goods goods = new Goods();
goods.setGoods_id(order.getGoods_id());
if(goodsService.getGoodsById(goods)==null) {
return ServerResponse.createByErrorMessage("商品不存在!");
}
//3. 一个用户不允许购买同一商品多次(在mysql中检查)
//ps:在redis中的检查已经封装到createOrderInRedis方法中
//由于会影响到测试效果,先注释掉
/*
String tel_num = (String)session.getAttribute("tel_num");
int goods_id = order.getGoods_id();
if(orderService.orderIsExist(tel_num,goods_id)) {
return ServerResponse.createByErrorMessage("您已抢购过该商品!请稍后再试!");
}
*/
//使用MD5算法加密用户信息和当前时间的字符串,构造不重复order_id
//(1)防止生成相同的订单号导致的创建订单失败
//(2)避免使用自增id,自增id能产生的订单数有局限性
String order_id = MD5Util.MD5EncodeUtf8(order.getTel_num()+order.getGoods_id()+DateTime.now().toString());
order.setOrder_id(order_id);
//4.如果该订单关联商品库存为0,返回“商品已抢光”
//否则继续进行
if(!goodsService.checkGoodsStockInRedis(order)) {
return ServerResponse.createByErrorMessage("商品库存已抢光!秒杀接口已关闭!");
}
//5.检查是否存在该订单
if(orderService.orderIsExistInRedis(order)) {
log.error("订单已经存在!");
return ServerResponse.createByErrorMessage("订单已经存在!");
}
//6.执行创建订单操作
try {
//6.1.在redis中创建订单,若失败,返回“创建订单失败”
if(orderService.createOrderInRedis(order) == 0) {
log.error("订单已经存在!");
return ServerResponse.createByErrorMessage("订单已经存在!");
}
//6.2.在redis中库存减一,若失败,撤回创建订单操作并返回“创建订单失败”
if(goodsService.decrGoodsStock(order) == 0) {
log.error("库存已抢光!");
return ServerResponse.createByErrorMessage("库存已抢光!");
}
} catch (Exception e) {
// TODO: handle exception
log.error("redis中创建订单失败!", e);
return ServerResponse.createByErrorMessage("创建订单失败!");
}
//创建定时器对象
//Timer t=new Timer();
//在3秒后执行MyTask类中的run方法
//t.schedule(new MyTask(order), 360000);
System.out.println("创建订单成功!");
return ServerResponse.createBySuccess("创建订单成功!", order);
}
其中注释的代码是之前用来计时判断创建订单操作是否失效,后来想到在redis有设置失效时间的操作,效果好太多
ps:这里给自己再提个醒!!这样启动计时器方法的非常不好,每次请求都会开启一个长时间存在的线程,可想而知,效果有多差了吧。
2.goodsService中的相关方法
goodsService接口类
public interface GoodsService extends BaseService<Goods>{
PageInfo<Map<String, Goods>> select(Goods goods,int pageNum,int pageSize);
PageInfo<Map<String, Goods>> selectById(Goods goods,int pageNum,int pageSize);
Goods getGoodsById(Goods goods);