秒杀业务的核心是库存处理,用户购买成功后会进行减库存操作,并记录购买明细。当秒杀开始时,大量用户同时发起请求,这是一个并行操作,多条更新库存数量的SQL语句会同时竞争秒杀商品所处数据库表里的那行数据,导致库存的减少数量与购买明细的增加数量不一致,因此,我们使用RabbitMQ进行削峰限流并且将请求数据串行处理。
首先我先设计了两张表,一张是秒杀库存表,另一张是秒杀成功表。
CREATE TABLE seckill
(
seckill_id BIGINT NOT NULL AUTO_INCREMENT COMMENT '商品库存id',
NAME VARCHAR(120) NOT NULL COMMENT '商品名称',
number INT NOT NULL COMMENT '库存数量',
initial_price BIGINT NOT NULL COMMENT '原价',
seckill_price BIGINT NOT NULL COMMENT '秒杀价',
sell_point VARCHAR(500) NOT NULL COMMENT '卖点',
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '秒杀创建时间',
start_time TIMESTAMP NOT NULL COMMENT '秒杀开始时间',
end_time TIMESTAMP NOT NULL COMMENT '秒杀结束时间',
PRIMARY KEY (seckill_id)
);
ALTER TABLE seckill COMMENT '秒杀库存表';
CREATE INDEX idx_create_time ON seckill
(
create_time
);
CREATE INDEX idx_start_time ON seckill
(
start_time
);
CREATE INDEX idx_end_time ON seckill
(
end_time
);
CREATE TABLE success_killed
(
success_id BIGINT NOT NULL AUTO_INCREMENT COMMENT '秒杀成功id',
seckill_id BIGINT NOT NULL COMMENT '秒杀商品id',
user_phone BIGINT NOT NULL COMMENT '用户手机号',
state TINYINT NOT NULL DEFAULT -1 COMMENT '状态标志:-1:无效;0:成功',
create_time TIMESTAMP NOT NULL COMMENT '秒杀成功创建时间',
PRIMARY KEY (success_id)
);
ALTER TABLE success_killed COMMENT '秒杀成功表';
CREATE INDEX idx_create_time ON success_killed
(
create_time
);
接下来我开始模拟用户请求,往RabbitMQ中发送100个手机号。
public String goods(@PathVariable("seckillId")Long seckillId){
for(int i = 100;i<200;i++){
seckillService.setGoods(seckillId,"13145678"+i);
}
return "success";
}
public void setGoods(Long seckillId,StringuserPhone){
String goods = seckillId+"/"+userPhone;
rabbitTemplate.convertAndSend("executeSeckill",goods);
}
然后我用RabbitMQ监听seckill_queue队列,当队列中接收到消息就会自动触发RabbitMQService类中的executeSeckill方法,消息将作为方法的参数传递进来执行秒杀操作。public class RabbitMQService {
@Autowired
privateSeckillMapper seckillMapper;
@Autowired
privateSuccessKilledMapper successKilledMapper;
privatestatic final Logger logger = Logger.getLogger(RabbitMQService.class);
publicvoid executeSeckill(String goods){
String[] good = goods.split("/");
Long seckillId = Long.parseLong(goods.substring(0, goods.indexOf('/')));
String userPhone = goods.substring(goods.lastIndexOf('/')+1);
Date nowTime = new Date();
Seckill seckill = seckillMapper.queryById(seckillId);
Date startTime = seckill.getStartTime();
Date endTime = seckill.getEndTime();
try {
//更新库存数量
int updateCount = seckillMapper.updateReduceNumber(seckillId, nowTime);
if(updateCount > 0) {
//记录购买行为
int insertCount = successKilledMapper.insertSuccessKilled(seckillId, userPhone);
}else {
logger.info("手机号为"+userPhone+"的用户秒杀失败");
}
}catch (RuntimeException e) {
//spring事务回滚只对运行期异常起作用
throw new RuntimeException("seckill error:" + e.getMessage());
}
}
}
最后我在前端页面使用倒计时插件增强用户体验效果。