最近公司准备做一个秒杀,翻阅了一些资料,通过不断地改进优化,终于设计出了一个比较稳定功能,我们项目中单独开了秒杀的自服务项目,以便于后期扩展,今天跟大家分享一下项目心得:
1.基本功能实现
最开始做出的1.0版本,话不多说上代码
@Override
public Object confirm(Integer productId,String accountId) {
if (StringUtils.isEmpty(productId)) {
return new CtrlResp<>("-1","商品id参数为空",null);
}
Address address = getUserDefaultAddress(accountId);
if (address == null) {
return new CtrlResp<>("-1","秒杀前请先填写默认地址",null);
}
Product product = productService.getProductById(productId);
if (product == null) {
return new CtrlResp<>("-1","商品不存在",null);
}
validSeckillTime(product);//时间验证
if (product.getStock() > 0) {
return new CtrlResp<>("-1","商品已抢完",null);
}
return confirmOrder(product,address);
}
/**
* 生成订单
* @param product
* @param address
* @return
*/
private CtrlResp confirmOrder(Product product,Address address) {
//填充订单信息
Order order = Order.builder().build();
//填充订单配送信息
//生产订单
Order orderData = null;
try {
orderData = orderService.createSeckillOrder(order);
}catch (Exception e) {
throw BizEx.build(BizExEnum.ERROR_001.getCode(),"创建订单失败");
}
return new CtrlResp<>("0","成功",orderData);
}
@Override
public Order createSeckillOrder(Order order) {
//订单信息插库
//减库存
List<OrderDetail> orderDetailList = order.getOrderDetailList();
for (OrderDetail orderDetail:orderDetailList) {
int updateNum = productService.updateStaockAfterSeckillSuccess(orderDetail.getProductId());
if (updateNum<=0) {
throw BizEx.build(BizExEnum.ERROR_001.getCode(),"更新库存失败");
}
}
return null;
}
<update id="updateStaockAfterSeckillSuccess">
update t_product set stock=stock-1.sellcount=sellcount+1 where 1=1 and id=#{productId} and stock>0
</update>
相信大家不难看出,这里使用的是数据库做的库存控制,到库存减少到0的时候后面的更新都不会在更新了,当然这个办法可以达到目的,由于数据库并不适合做并发量很大的操作,当并发量很高的情况下,这个方式肯定是不行的,显然秒杀这种业务场景都是发生在很高的并发量下,所以接下来进行
2. 加入缓存
我们加入缓存,将库存数用缓存进行控制,首先我们先将每个秒杀商品的库存加载到缓存中
@PostConstruct
public void init() {
List<Product> products = productMapper.selectByMap(new ImmutableMap.Builder<String,Object>()
.put("type", ProductType.SECKILL)
.build());
for (Product product:products) {
keyValueService.set(RedisConstants.PRODUCT_STOCK+"_"+product.getId(),product.getStock());
}
}
请求过来时查看缓存中的库存是否充足,充足则放行,不充足则提示商品已抢完,如果下单存在异常或退单的时候,我们要将库存进行还原,这里还有一个问题,就是随着最后一个商品被消耗掉,后面的请求都会被一一拦截掉,导致缓存stock会被减出一个很大的负数,这个时候如果有退单的情况,缓存中数量加1,这个时候的库存书依然为负数,后面在来请求依然无法获取到退掉的这个商品,所有我们在发现商品耗尽时,将缓存中数量及时还原成0,这个在发生退单时,库存数从0进行累加,下一个请求就可以获取到
通过Jmeter压测,这个时候性能已经提升了很多,虽然访问redis的速度很快,但还是会带来一定的性能消耗,其实在商品库存被消耗完的那一刻,库存对缓存的依赖就不需要了,这个时候我们完全可以在jvm内存在为商品是否已经被消耗完打个标记,这个就大大减少了对redis访问的压力,响应速度也会进一步提升
首先定义一个map
private static ConcurrentHashMap<String,Boolean> productSoldOutMap = new ConcurrentHashMap<>();
private static ConcurrentHashMap<String,Boolean> getProductSoldOutMap() {
return productSoldOutMap;
}
当请求过来时在检查缓存之前,检查一下内存中的标识,库存消耗完时,我们将该商品已消耗完的表示写道内存中,一旦出现有退单或下单失败,我们清除掉商品对应的标识
这里有产生了一个问题,一般我们使用的微服务系统,同一个服务不止一台,各自维护各自的内存,比如a机器上可能标记已经是消耗完,b机器上还没有这个标记,还有退单的情况,如果对产品要求不是很严格的情况下,因为还有缓存的控制,这个问题也不是那么严重,秒杀本身也是一个运气活。
当然我们有解决的办法,这个时候我们可以使用zookeeper,利用zookeeper回调达到多级缓存的一致性
3.异步下单
运行可以看出,下单的过程是个相对耗时的操作,我们能不能将下单异步化喃,答案是可定的,我们继续进优化,将下单异步处理
定义监听器,将订单生成的逻辑搬移过来
由于下单的过程需要处理相应的逻辑,这里完全会有失败的情况,怎么办喃?这个我们要结合前端同步优化,当抢购成功后,让前端页面进入等待状态,定时轮询后端结果,我们可以订单对应的状态码,-1抢购失败,0成功,1继续等待,等待的过程中前端可以给一个友好的提示,比如排队中,当处理成功后,将信息放入缓存
加入防重复校验
到此基本功能已经完成,上面的代码只是将核心的逻辑展示给大家,具体下单业务还请大家根据自己的业务填充。还有一部分安全优化和限流防刷,下期继续和大家分享。。。
关注下方公众号,了解更多,回复关键字「面试」,更有好礼相送
👇👇👇