引言
本文代码已提交至Github(版本号:
2c985822b282756e3fd70490cb0ba6f4f2140e47
),有兴趣的同学可以下载来看看:https://github.com/ylw-github/taodong-shop
秒杀系统在前面已经讲解了“前端优化”
以及“防止库存超卖”
的功能,但是在效率这一块还是很慢的,那么后台的秒杀完整代码流程是如何的呢?本文来讲解下,阅读前,童鞋们可以先阅读之前写的博客:
本文目录结构:
l____引言
l____ 1.秒杀原理图
l____ 2. 后台核心代码
l________ 2.1 令牌桶生成接口
l________ 2.2 秒杀接口(核心)
l________________ 2.2.1 MQ配置
l________________ 2.2.2 生产者
l________________ 2.2.3 消费者
l________ 2.3 用户查询接口
l____ 3. 测试
1.秒杀原理图
下面贴上我自己整理的原理图,如下:
从原理图,可以看到秒杀的流程大致如下:
- 商户添加秒杀商品的时候,后台会自动从Redis里生成令牌桶,如商品A的库存有100个,那么当用户修改商品时会去Redis里添加一条数据,格式:商品id+List令牌桶(数量是库存数量)。
- 用户抢购时,会从令牌桶里获取令牌,如果能获取成功,则通过MQ去异步修改数据库里面的订单表以及秒杀表。
- 抢购完成后,会提示用户“正在排队中…”,用户需要自己主动的去查询抢购结果。
2. 后台核心代码
2.1 令牌桶生成接口
令牌桶生成接口核心代码:
@Override
public BaseResponse<JSONObject> addSpikeToken(Long seckillId, Long tokenQuantity) {
// 1.验证参数
if (seckillId == null) {
return setResultError("商品库存id不能为空!");
}
if (tokenQuantity == null) {
return setResultError("token数量不能为空!");
}
SeckillEntity seckillEntity = seckillMapper.findBySeckillId(seckillId);
if (seckillEntity == null) {
return setResultError("商品信息不存在!");
}
// 2.使用多线程异步生产令牌
createSeckillToken(seckillId, tokenQuantity);
return setResultSuccess("令牌正在生成中.....");
}
@Async
public void createSeckillToken(Long seckillId, Long tokenQuantity) {
generateToken.createListToken("seckill_", seckillId + "", tokenQuantity);
}
Redis令牌桶生成工具类:
①GenerateToken
public void createListToken(String keyPrefix, String redisKey, Long tokenQuantity) {
List<String> listToken = getListToken(keyPrefix, tokenQuantity);
redisUtil.setList(redisKey, listToken);
}
public List<String> getListToken(String keyPrefix, Long tokenQuantity) {
List<String> listToken = new ArrayList<>();
for (int i = 0; i < tokenQuantity; i++) {
String token = keyPrefix + UUID.randomUUID().toString().replace("-", "");
listToken.add(token);
}
return listToken;
}
②RedisUtil:
public void setList(String key, List<String> listToken) {
stringRedisTemplate.opsForList().leftPushAll(key, listToken);
}
2.2 秒杀接口(核心)
2.2.1 MQ配置
①application.yml配置:
rabbitmq:
####连接地址
host: 127.0.0.1
####端口号
port: 5672
####账号
username: guest
####密码
password: guest
### 地址
virtual-host: spike_host
listener:
simple:
retry:
####开启消费者(程序出现异常的情况下会)进行重试
enabled: true
####最大重试次数
max-attempts: 5
####重试间隔时间
initial-interval: 1000
####开启手动ack
acknowledge-mode: manual
default-requeue-rejected: false
②RabbitMQ配置:
/**
* description: RabbitmqConfig 配置
* create by: YangLinWei
* create time: 2020/5/26 10:54 上午
*/
@Component
public class RabbitmqConfig {
// 添加修改库存队列
public static final String MODIFY_INVENTORY_QUEUE = "modify_inventory_queue";
// 交换机名称
private static final String MODIFY_EXCHANGE_NAME = "modify_exchange_name";
// 1.添加交换机队列
@Bean
public Queue directModifyInventoryQueue() {
return new Queue(MODIFY_INVENTORY_QUEUE);
}
// 2.定义交换机
@Bean
DirectExchange directModifyExchange() {
return new DirectExchange(MODIFY_EXCHANGE_NAME);
}
// 3.修改库存队列绑定交换机
@Bean
Binding bindingExchangeintegralDicQueue() {
return BindingBuilder.bind(directModifyInventoryQueue()).to(directModifyExchange()).with("modifyRoutingKey");
}
}
2.2.2 生产者
/**
* description: 秒杀生产者
* create by: YangLinWei
* create time: 2020/5/26 10:58 上午
*/
@Component
@Slf4j
public class SpikeCommodityProducer implements RabbitTemplate.ConfirmCallback {
@Autowired
private RabbitTemplate rabbitTemplate;
@Transactional
public void send(JSONObject jsonObject) {
String jsonString = jsonObject.toJSONString();
System.out.println("jsonString:" + jsonString);
String messAgeId = UUID.randomUUID().toString().replace("-", "");
// 封装消息
Message message = MessageBuilder.withBody(jsonString.getBytes())
.setContentType(MessageProperties.CONTENT_TYPE_JSON).setContentEncoding("utf-8").setMessageId(messAgeId)
.build();
// 构建回调返回的数据(消息id)
this.rabbitTemplate.setMandatory(true);
this.rabbitTemplate.setConfirmCallback(this);
CorrelationData correlationData = new CorrelationData(jsonString);
rabbitTemplate.convertAndSend("modify_exchange_name", "modifyRoutingKey", message, correlationData);
}
// 生产消息确认机制 生产者往服务器端发送消息的时候,采用应答机制
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
String jsonString = correlationData.getId();
System.out.println("消息id:" + correlationData.getId());
if (ack) {
log.info(">>>使用MQ消息确认机制确保消息一定要投递到MQ中成功");
return;
}
JSONObject jsonObject = JSONObject.parseObject(jsonString);
// 生产者消息投递失败的话,采用递归重试机制
send(jsonObject);
log.info(">>>使用MQ消息确认机制投递到MQ中失败");
}
}
2.2.3 消费者
/**
* description: 库存消费者
* create by: YangLinWei
* create time: 2020/5/26 10:59 上午
*/
@Component
@Slf4j
public class StockConsumer {
@Autowired
private SeckillMapper seckillMapper;
@Autowired
private OrderMapper orderMapper;
@RabbitListener(queues = "modify_inventory_queue")
@Transactional
public void process(Message message, @Headers Map<String, Object> headers, Channel channel) throws IOException {
String messageId = message.getMessageProperties().getMessageId();
String msg = new String(message.getBody(), "UTF-8");
log.info(">>>messageId:{},msg:{}", messageId, msg);
JSONObject jsonObject = JSONObject.parseObject(msg);
// 1.获取秒杀id
Long seckillId = jsonObject.getLong("seckillId");
SeckillEntity seckillEntity = seckillMapper.findBySeckillId(seckillId);
if (seckillEntity == null) {
log.warn("seckillId:{},商品信息不存在!", seckillId);
basicNack(message, channel);
return;
}
Long version = seckillEntity.getVersion();
int inventoryDeduction = seckillMapper.optimisticDeduction(seckillId, version);
if (!toDaoResult(inventoryDeduction)) {
log.info(">>>seckillId:{}修改库存失败>>>>inventoryDeduction返回为{} 秒杀失败!", seckillId, inventoryDeduction);
basicNack(message, channel);
return;
}
// 2.添加秒杀订单
OrderEntity orderEntity = new OrderEntity();
String phone = jsonObject.getString("phone");
orderEntity.setUserPhone(phone);
orderEntity.setSeckillId(seckillId);
orderEntity.setState(1l);
int insertOrder = orderMapper.insertOrder(orderEntity);
if (!toDaoResult(insertOrder)) {
basicNack(message, channel);
return;
}
log.info(">>>修改库存成功seckillId:{}>>>>inventoryDeduction返回为{} 秒杀成功", seckillId, inventoryDeduction);
basicNack(message, channel);
}
// 调用数据库层判断
public Boolean toDaoResult(int result) {
return result > 0 ? true : false;
}
// 消费者获取到消息之后 手动签收 通知MQ删除该消息
private void basicNack(Message message, Channel channel) throws IOException {
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
}
}
2.3 用户查询接口
@RestController
public class OrderSeckillServiceImpl extends BaseApiService<JSONObject> implements OrderSeckillService {
@Autowired
private OrderMapper orderMapper;
@Override
public BaseResponse<JSONObject> getOrder(String phone, Long seckillId) {
if (StringUtils.isEmpty(phone)) {
return setResultError("手机号码不能为空!");
}
if (seckillId == null) {
return setResultError("商品库存id不能为空!");
}
OrderEntity orderEntity = orderMapper.findByOrder(phone, seckillId);
if (orderEntity == null) {
return setResultError("正在排队中.....");
}
return setResultSuccess("恭喜你秒杀成功!");
}
}
3. 测试
①模拟用户修改商品库存,更新令牌桶,浏览器访问:http://localhost:9800/addSpikeToken?seckillId=100001&tokenQuantity=100
可以看到Redis里生成商品key id为100001,值为list,大小为100的集合:
②模拟抢购,浏览器访问:http://localhost:9800/spike?phone=13800000001&seckillId=100001
可以看到数据库库存减一:
订单并生成了一条记录:
Redis减少了一个令牌:
③模拟用户查询抢购结果,浏览器访问: