项目场景
实现一个商品秒杀的功能,能后台自定义秒杀时间段、商品库存等信息。
一、设计思路
这里简单分享下思路:
1.限流
秒杀时大量用户会在同一时间同时进行抢购,网站瞬时访问流量激增,由于只有少部分用户能够秒杀成功,所以要限制大部分流量,只允许少部分流量进入服务后端。这里使用基于Redis简单粗暴的限流方案:信号量(Semaphore)
信号量就是可以被 多个线程同时持有 的 一种同步对象,比如我设置一个值为5的计数信号量,那么现在有十个线程来获取他就只会有五个可以成功,剩下那五个则获取失败。
所以说如果有个计数信号量定义的值是1,那么他其实就等同于 mutex (互斥锁)
2.具体流程
这里首先需要明确如何缓存秒杀的商品信息?
- 这里是根据秒杀活动状态来缓存秒杀的商品信息,上下架秒杀活动就会将商品信息缓存到Redis中。
- 秒杀成功使用消息队列发送订单信息,RabbitMQ监听器在接受到消息后,将订单信息写入数据库。
- 在秒杀时使用redisson对商品信息上锁,由于打算使用信号量,而信号量本质就是一种锁,所以不用再加锁。
二、数据库设计
1.秒杀活动表
存储秒杀活动信息seckill_info
字段 |
描述 |
id(varchar) |
主键id |
name(varchar) |
活动名称 |
start_time(datetime) |
开始时间 |
end_time(datetime) |
结束时间 |
description(varchar) |
描述 |
state(int) |
活动状态,1 开启 2 关闭 |
… |
|
2.秒杀商品表
存储秒杀商品及相关规则信息seckill_sku_info
字段 |
描述 |
id(varchar) |
主键id |
seckill_id(varchar) |
秒杀活动id |
sku_id(varchar) |
商品id |
sku_name(varchar) |
商品名字 |
seckill_price(decimal) |
秒杀价格 |
seckill_stock(int) |
秒杀总数量(库存) |
limit(int) |
每人限购数量 |
… |
|
3.秒杀订单表
存储订单信息seckill_order
字段 |
描述 |
id(varchar) |
主键id |
order_id(varchar) |
订单号 |
seckill_id(varchar) |
秒杀活动id |
user_id(datetime) |
用户id |
user_name(datetime) |
用户名 |
seckill_price(decimal) |
秒杀价格 |
seckill_num(int) |
秒杀数量 |
sku_id(varchar) |
商品id |
sku_name(varchar) |
商品名字 |
… |
|
(上面数据库只列举了主要字段,具体业务需要具体设计)
三、业务逻辑
1.提供接口
(1)启用/停用活动将秒杀活动和秒杀商品信息缓存到redis
(2)前台获取秒杀活动
(3)用户参与秒杀
(4)秒杀下单
2.核心代码
缓存秒杀活动及商品信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| BoundHashOperations<String,String,Object> seckills = stringRedisTemplate.boundHashOps("seckill:demo"); seckills.put(seckillInfo.getId(),JSON.toJSONString(seckillInfo));
BoundHashOperations<String,String,Object> skus = stringRedisTemplate.boundHashOps("seckill:skus:"+seckillInfo.getId());
List<SeckillSkuInfo> skuInfos = seckillSkuInfoService.listBySecKillId(seckillInfo.getId()); skuInfos.forEach(item->{ String json = JSON.toJSONString(item); skus.put(item.getId(),json); RSemaphore semaphore = redissonClient.getSemaphore("seckill:stock:"+item.getId()); semaphore.trySetPermits(item.getSeckillStock()); stringRedisTemplate.expire("seckill:stock:"+item.getId(),seckillInfo.getEndTime().getTime()-System.currentTimeMillis(),TimeUnit.MILLISECONDS); });
|
秒杀逻辑代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| BoundHashOperations<String,String,Object> seckills = stringRedisTemplate.boundHashOps("seckill:demo");
SeckillInfo info = JSON.parseObject(seckills.get(seckillId).toString(), SeckillInfo.class);
...
BoundHashOperations<String, String, String> skus = stringRedisTemplate.boundHashOps("seckill:skus:"+seckillId); String skuInfoStr = skus.get(id); if(StringUtils.isNotBlank(skuInfoStr)){ SeckillSkuInfo skuInfo = JSON.parseObject(skuInfoStr, SeckillSkuInfo.class); RSemaphore semaphore = redissonClient.getSemaphore("seckill:stock:"+skuInfo.getId()); int count = semaphore.availablePermits(); if (count == 0) { ... } boolean flag = semaphore.tryAcquire(); if (flag) { long total = stringRedisTemplate.opsForValue().increment("seckill:user:"+userId+"_"+info.getId(),1); stringRedisTemplate.expire("seckill:user:"+userId+"_"+info.getId(),endTime-time, TimeUnit.MILLISECONDS); if (不满足条件){ semaphore.release(num); ... } SeckillOrder order = new SeckillOrder(); ... rabbitTemplate.convertAndSend("seckillExchange", "seckillOrderRoutingkey",order); } else { ... } }else { ... }
|
使用mq异步下单,处理订单、库存相关信息。