开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 5 天,点击查看活动详情
每日英语:
Laughter is an instant vacation.
笑是即刻的假期。 -米尔顿·伯利
热门商品抢单
热门商品抢单分为2个步骤:
1:需要识别商品是否为热门商品,如果是热门商品则需要排队,排队需要向RocketMQ发送MQ消息。
2:下单需要从排队信息中获取抢单信息,并执行下单操作。
接下来我们按这2个步骤实现。
1 抢单排队
排队的时间节点很重要,不要在后端微服务排队,后端服务排队会降低整个服务的性能,我们可以选择在代理层排队(Nginx)或者Api网关层排队(Gateway),由于大家的专长是Java,我们选择在API网关排队。
1)引入依赖
在mall-api-gateway的pom.xml中引入如下依赖:
<dependencies>
<!--Redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--rocketmq-->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.0.2</version>
</dependency>
</dependencies>
2)MQ、Redis配置
修改mall-api-gateway的bootstrap.yml文件,添加RocketMQ和Redis配置:
spring:
redis:
host: 192.168.xxx.xxx
port: 6379
#producer
rocketmq:
name-server: 192.168.xxx.xxx:9876
producer:
group: hotorder-group
send-message-timeout: 300000
compress-message-body-threshold: 4096
max-message-size: 4194304
retry-times-when-send-async-failed: 0
retry-next-server: true
retry-times-when-send-failed: 2
3)热门商品排队
在mall-api-gateway中创建com.xz.mall.api.hot.HotQueue在该类中进行排队操作:
@Component
public class HotQueue {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private RocketMQTemplate rocketMQTemplate;
//商品非热门
public static final Integer NOT_HOT=0;
//已经在排队中
public static final Integer HAS_QUEUE=204;
//排队成功
public static final Integer QUEUE_ING=200;
/***
* 抢单排队
* username:用户名
* id:商品ID
* num:件数
*/
public int hotToQueue(String username,String id,Integer num){
//获取该商品在Redis中的信息,如果Redis中存在对应信息,热门商品
Boolean bo = redisTemplate.boundHashOps("HotSeckillGoods").hasKey(id);
if(!bo){
//商品非热门
return NOT_HOT;
}
//避免重复排队
Long increment = redisTemplate.boundValueOps("OrderQueue" + username).increment(1);
if(increment>1){
//请勿重新排队
return HAS_QUEUE;
}
//执行排队操作
Map<String,Object> dataMap = new HashMap<String,Object>();
dataMap.put("username",username);
dataMap.put("id",id);
dataMap.put("num",num);
Message<String> message = MessageBuilder.withPayload(JSON.toJSONString(dataMap)).build();
rocketMQTemplate.convertAndSend("order-queue",message);
return QUEUE_ING;
}
}
在ApiFilter中调用上面的排队方法:
@Autowired
private HotQueue hotQueue;
/***
* 执行拦截处理 http://localhost:9001/mall/seckill/order?id&num
* JWT
* @param exchange
* @param chain
* @return
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
//用户名
String username = "xz";
//商品ID
String id = request.getQueryParams().getFirst("id");
//数量
Integer num =Integer.valueOf( request.getQueryParams().getFirst("num") );
//排队结果
int result = hotQueue.hotToQueue(username, id, num);
//QUEUE_ING、HAS_QUEUE
if(result==HotQueue.QUEUE_ING || result==HotQueue.HAS_QUEUE){
//响应状态码200
Map<String,Object> resultMap = new HashMap<String,Object>();
resultMap.put("type","hot");
resultMap.put("code",result);
exchange.getResponse().setStatusCode(HttpStatus.OK);
exchange.getResponse().setComplete();
exchange.getResponse().getHeaders().add("message",JSON.toJSONString(resultMap));
}
//NOT_HOT 直接由后端服务处理
return chain.filter(exchange);
}
2 排队监听
用户排队后,我们需要对用户排队的信息进行下单操作,此时需要监听排队信息,我们在mall-seckill-service中编写监听操作,用于监听消息。
1)引入依赖
修改mall-seckill-service中的pom.xml添加rocketmq依赖:
<!--rocketmq-->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.0.2</version>
</dependency>
2)配置MQ信息
修改bootstrap.yml添加rocketmq配置:
#mq
rocketmq:
name-server: 192.168.xxx.xxx:9876
3)监听创建
创建com.xz.mall.seckill.mq.OrderQueueListener实现对排队抢单信息监听:
@RocketMQMessageListener(
topic = "order-queue", //topic:和消费者发送的topic相同
consumerGroup = "orderqueue-consumer", //group:不用和生产者group相同
selectorExpression = "*") //tag
@Component
public class OrderQueueListener implements RocketMQListener {
/***
* 排队信息
* @param message
*/
@Override
public void onMessage(Object message) {
System.out.println("排队信息:"+message);
}
}
3 热门商品抢单实现
抢单实现我们需要监听队列信息,并实现下单操作,同时如果抢单后商品库存为0,则需要同步到数据库中。
1)Service
接口:修改com.xz.mall.seckill.service.SeckillOrderService添加热门商品下单方法:
/***
* 热门商品抢单操作
*/
int add(Map<String,Object> dataMap);
实现类:修改com.xz.mall.seckill.service.impl.SeckillOrderServiceImpl实现热门商品下单:
@Service
public class SeckillOrderServiceImpl extends ServiceImpl<SeckillOrderMapper,SeckillOrder> implements SeckillOrderService {
//库存不足
public static final int STORE_NOT_FULL=0;
//库存足够下单成功
public static final int STORE_FULL_ORDER_SUCCESS=1;
@Autowired
private SeckillOrderMapper seckillOrderMapper;
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private SeckillGoodsMapper seckillGoodsMapper;
/***
* 热门商品抢单实现
* @return
*/
@Override
public int add(Map<String,Object> dataMap) {
//username
String username = dataMap.get("username").toString();
//id
String id = dataMap.get("id").toString();
//num
Integer num =Integer.valueOf(dataMap.get("num").toString() );
/**
* 库存足够
*/
Object storecount = redisTemplate.boundHashOps("HotSeckillGoods").get(id);
if(storecount==null || Integer.valueOf(storecount.toString())<num){
//移除排队标识
redisTemplate.delete("OrderQueue"+username);
return STORE_NOT_FULL;
}
//查询商品信息
SeckillGoods seckillGoods = seckillGoodsMapper.selectById(id);
/***
* 添加订单
*/
SeckillOrder seckillOrder = new SeckillOrder();
seckillOrder.setUsername(username);
seckillOrder.setSeckillGoodsId(id);
seckillOrder.setCreateTime(new Date());
seckillOrder.setMoney(seckillGoods.getSeckillPrice()*num);
seckillOrder.setNum(num);
seckillOrder.setStatus(0); //下单了
seckillOrderMapper.insert(seckillOrder);
/*****
* 库存递减
*/
Long lastStoreCount = redisTemplate.boundHashOps("HotSeckillGoods").increment(id, -num);
if(lastStoreCount==0){
//将数据同步到数据库
seckillGoods = new SeckillGoods();
seckillGoods.setId(id);
seckillGoods.setStoreCount(0);
seckillGoodsMapper.updateById(seckillGoods);
//删除Redis缓存
redisTemplate.boundHashOps("HotSeckillGoods").delete(id);
}
//移除排队标识
redisTemplate.delete("OrderQueue"+username);
return STORE_FULL_ORDER_SUCCESS;
}
}
4 抢单超卖控制
在热门商品抢单过程中,存在超卖现象,即便是MQ监听下单,但MQ消费组有可能会有集群,各个组集群消费时,判断商品库存有可能同时出现判断商品库存为1的时候,这时候就容易存在超卖现象。
我们可以采用基于Redis的分布式锁来实现。
1)引入依赖包
<!--Redisson分布式锁-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.8.2</version>
</dependency>
2)创建RedissonClient
创建com.xz.mall.seckill.lock.RedissonConfig用于配置当前使用的Redis服务
@Configuration
public class RedissonConfig {
/***
* 创建RedissonClient客户端
* @return
*/
public RedissonClient redisson(){
Config config = new Config();
//单机模式
config.useSingleServer().setAddress("redis://192.168.xxx.xxx:6379");
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
}
3)超卖控制
修改com.xz.mall.seckill.service.impl.SeckillOrderServiceImpl的热门商品抢单方法,增加分布式锁控制:
@Autowired
private RedissonClient redissonClient;
/***
* 热门商品抢单实现
* @return
*/
@Override
public int add(Map<String,Object> dataMap) {
//username
String username = dataMap.get("username").toString();
//id
String id = dataMap.get("id").toString();
//num
Integer num =Integer.valueOf(dataMap.get("num").toString() );
//获取锁
RLock lock = redissonClient.getLock("No00001");
lock.lock();
try {
/**
* 库存足够 略...
*/
//略...
if(lastStoreCount==0){
//略...
}
//移除排队标识
redisTemplate.delete("OrderQueue"+username);
//释放锁
lock.unlock();
} catch (NumberFormatException e) {
//释放锁
lock.unlock();
}
return STORE_FULL_ORDER_SUCCESS;
}
完整代码如下:
@Autowired
private RedissonClient redissonClient;
/***
* 热门商品抢单实现
* @return
*/
@Override
public int add(Map<String,Object> dataMap) {
//username
String username = dataMap.get("username").toString();
//id
String id = dataMap.get("id").toString();
//num
Integer num =Integer.valueOf(dataMap.get("num").toString() );
//获取锁
RLock lock = redissonClient.getLock("No00001");
lock.lock();
try {
/**
* 库存足够
*/
Object storecount = redisTemplate.boundHashOps("HotSeckillGoods").get(id);
if(storecount==null || Integer.valueOf(storecount.toString())<num){
//移除排队标识
redisTemplate.delete("OrderQueue"+username);
return STORE_NOT_FULL;
}
//查询商品信息
SeckillGoods seckillGoods = seckillGoodsMapper.selectById(id);
/***
* 添加订单
*/
SeckillOrder seckillOrder = new SeckillOrder();
seckillOrder.setUsername(username);
seckillOrder.setSeckillGoodsId(id);
seckillOrder.setCreateTime(new Date());
seckillOrder.setMoney(seckillGoods.getSeckillPrice()*num);
seckillOrder.setNum(num);
seckillOrder.setStatus(0); //下单了
seckillOrderMapper.insert(seckillOrder);
/*****
* 库存递减
*/
Long lastStoreCount = redisTemplate.boundHashOps("HotSeckillGoods").increment(id, -num);
if(lastStoreCount==0){
//将数据同步到数据库
seckillGoods = new SeckillGoods();
seckillGoods.setId(id);
seckillGoods.setStoreCount(0);
seckillGoodsMapper.updateById(seckillGoods);
//删除Redis缓存
redisTemplate.boundHashOps("HotSeckillGoods").delete(id);
}
//移除排队标识
redisTemplate.delete("OrderQueue"+username);
lock.unlock();
} catch (NumberFormatException e) {
lock.unlock();
}
return STORE_FULL_ORDER_SUCCESS;
}
总结
本篇主要讲述了一下热门商品抢单实现时,需要关注的几个问题,抢单排队、排队监听、抢单超卖控制实现。