前言
電商平臺有時候會搞一些秒殺活動,秒殺活動的併發量特別高,會導致訪問變慢、商品超賣等問題。所以,寫一篇博客記錄一下主要實現思路,解決一些小問題。
一、建表
就簡單的建一張表,秒殺活動表。然後就可以寫代碼,通過頁面添加秒殺活動了,這裏就不寫這些了。
CREATE TABLE `seckill_promotion_table` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`goods_id` varchar(20) DEFAULT '0' COMMENT '商品id',
`ps_count` int(11) DEFAULT NULL COMMENT '秒殺數量',
`current_price` decimal(10,2) DEFAULT NULL COMMENT '秒殺價格',
`start_time` datetime DEFAULT NULL COMMENT '開始時間',
`end_time` datetime DEFAULT NULL COMMENT '結束時間',
`status` int(11) NOT NULL DEFAULT '0' COMMENT '0-未開始 1-進行中 2-已結束',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='秒殺活動表';
二、數據靜態化
把商品信息等等一些不經常改變的數據都寫死在html頁面裏,不用從後臺獲取,這樣秒殺的時候就能加快用戶的訪問速度,秒殺倒計時和評論信息可以從後臺獲取進行渲染。可以使用thymeleaf或者freemarker來渲染,然後生成html文件,直接放在nginx裏就可以訪問了。這個也很簡單,就不在贅述了。
三、初始化進redis,防止超賣
添加秒殺活動的時候會設置開始時間和結束時間。設置的開始時間並不是馬上就開始,所以我們就需要定時任務去查詢當前是否有開始的秒殺任務。
然後把要開始的秒殺任務扔進redis裏,爲什麼要扔進redis裏呢?因爲redis操作都是串行的,同時又好多請求的時候,會排隊一個一個的執行,防止商品的超賣。所以, 有幾個要秒殺的商品,我們就初始化幾個list對象放進redis裏。
@Mapper
public interface SeckillPromotionLWQDAO{
@InsertProvider(method = "insertSeckillPromotion", type = SeckillPromotionLWQProvider.class)
@Options(useGeneratedKeys = true,keyProperty = "seckillpromotion.id")
Integer insert(@Param("seckillpromotion")SeckillPromotionDO seckillpromotion)throws Exception;
@UpdateProvider(method = "updateSeckillPromotion", type = SeckillPromotionLWQProvider.class)
Integer updateById(@Param("seckillpromotion")SeckillPromotionDO seckillpromotion)throws Exception;
@Select("SELECT id,goods_id,ps_count,current_price,start_time,end_time,status FROM seckill_promotion_table "
+ "WHERE id = #{id}")
SeckillPromotionDO getByPrimaryKey(Integer id)throws Exception;
@Select("SELECT id,goods_id,ps_count,current_price,start_time,end_time,status FROM seckill_promotion_table ")
List<SeckillPromotionDO> listAllSeckillPromotionDO()throws Exception;
/**
* 查詢未開始的秒殺動
* @return
* @throws Exception
*/
@Select("SELECT id,goods_id,ps_count,current_price,start_time,end_time,status "
+ "FROM seckill_promotion_table "
+ "WHERE now() BETWEEN start_time AND end_time AND status = 0")
List<SeckillPromotionDO> listUnstartSeckill()throws Exception;
/**
* 查詢已經過期的秒殺活動
* @return
* @throws Exception
*/
@Select("SELECT id,goods_id,ps_count,current_price,start_time,end_time,status "
+ "FROM seckill_promotion_table "
+ "WHERE now() > end_time AND status = 1")
List<SeckillPromotionDO> listExpireSeckill()throws Exception;
}
@Component
public class SeckillTask {
private static final String PREFIX = "seckill:count:";
@Autowired
private SeckillPromotionLWQDAO seckillDAO;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Scheduled(cron = "0/5 * * * * ?")
public void startSecKill() throws Exception{
List<SeckillPromotionDO> list = seckillDAO.listUnstartSeckill();
if (!list.isEmpty()) {
for (SeckillPromotionDO seckill : list) {
//刪掉以前重複的活動任務緩存
redisTemplate.delete(PREFIX + seckill.getId());
//有幾個庫存商品,則初始化幾個list對象
for(int i = 0 ; i < seckill.getPsCount() ; i++){
redisTemplate.opsForList().rightPush(PREFIX + seckill.getId(), seckill.getGoodsId());
}
seckill.setStatus(1);
seckillDAO.updateById(seckill);
}
}
}
@Scheduled(cron = "0/5 * * * * ?")
public void endSecKill() throws Exception{
List<SeckillPromotionDO> list = seckillDAO.listExpireSeckill();
if (! list.isEmpty()) {
for (SeckillPromotionDO seckill : list) {
seckill.setStatus(2);
seckillDAO.updateById(seckill);
redisTemplate.delete(PREFIX + seckill.getId());
}
}
}
}
四、開始秒殺
接下來就是秒殺商品了,我們會去redis裏取上一步放進去的list,如果能取到就是秒殺成功了,然後把用戶id放進redis的set裏,這樣秒殺的用戶就不回重複了。
搶到商品後,生成訂單信息,然後把訂單信息扔進MQ裏,用於削峯處理。
public ReturnDataDTO<Object> processSeckill(String userId, Integer seckillId) throws Exception {
SeckillPromotionDO seckill = seckillDAO.getByPrimaryKey(seckillId);
if (Objects.equals(seckill, null)) {
throw new MyException(LwqExceptionEnum.NOT_EXIT);
}
Integer status = seckill.getStatus();
if (status == 0) {
throw new MyException(LwqExceptionEnum.NOT_START);
}
if (status == 2) {
throw new MyException(LwqExceptionEnum.EVER_END);
}
String goodsIdInRedis = (String) redisTemplate.opsForList().leftPop(GOODS_PREFIX + seckill.getId());
if (goodsIdInRedis != null) {
//判斷是否已經搶購過
boolean isExisted = redisTemplate.opsForSet().isMember(USER_PREFIX + seckill.getId(), userId);
if (!isExisted) {
//搶到商品了
redisTemplate.opsForSet().add("seckill:users:" + seckill.getId(), userId);
}else{
//已經搶購過的用戶,再把商品id放回去
redisTemplate.opsForList().rightPush("seckill:count:" + seckill.getId(), seckill.getGoodsId());
throw new MyException(LwqExceptionEnum.EVER_SECKILL);
}
} else {
throw new MyException(1018,"抱歉,該商品已被搶光,下次再來吧!!");
}
String goodsId = seckill.getGoodsId();
GoodsDO goodsDO = goodsDAO.getByPrimaryKey(goodsId);
if (Objects.equals(goodsDO, null)) {
throw new MyException(1019, "沒有該商品");
}
BigDecimal currentPrice = goodsDO.getCurrentPrice();
String orderNumber = UUID.randomUUID().toString();
Snowflake snowflake = IdUtil.createSnowflake(1, 1);
String id = snowflake.nextIdStr();
GoodsOrderDO order = new GoodsOrderDO();
order.setId(id);
order.setOrderNumber(orderNumber);
order.setUserId(userId);
order.setTotalAmount(currentPrice);
order.setStatus(1);
//把訂單信息發送到MQ,用於削峯處理
JSONObject json = (JSONObject) JSONObject.toJSON(order);
seckillProducer.sendSeckillOrderMessage(json);
//返給前端訂單號,用於主動查詢訂單是否創建成功
JSONObject data = new JSONObject(true);
data.put("userId", userId);
data.put("orderNumber", orderNumber);
return ReturnDataDTO.ok(data);
}
五、生成訂單
MQ的消費者就可以把訂單信息入庫,這樣就生成了訂單。
用戶端就可以根據訂單號去查詢是否生成訂單了。MQ消費者訂單沒有入庫的時候,就給用戶展示訂單正在生成中等提示信息。
有了訂單信息就可以去支付了。。
六、寫在最後的話
主要是貼了一些主要的代碼,思路就時有多少秒殺商品就初始化幾個list到redis裏,秒殺的時候去redis裏拿list,能拿到就說明秒殺成功(注意判斷用戶的重複),就把用戶放進set裏,生成訂單信息,扔進MQ。這時候就輪訓訂單是否生成。