前言
电商平台有时候会搞一些秒杀活动,秒杀活动的并发量特别高,会导致访问变慢、商品超卖等问题。所以,写一篇博客记录一下主要实现思路,解决一些小问题。
一、建表
就简单的建一张表,秒杀活动表。然后就可以写代码,通过页面添加秒杀活动了,这里就不写这些了。
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。这时候就轮训订单是否生成。