8.用户抢购
业务核心关键
使用redis的锁操作对库存、用户等属性进行加锁解锁的操作实现抢购逻辑
业务流程
抢购业务实现
- 加了一万个校验的购买流程
- springboot、dobbu、activemq、redis
- 用户点击购买->初始化库存->锁用户->库存数量校验->锁库存->生成内部订单->排队生成外部订单->等待订单生成->扣库存->释放库存锁
web模块负责:
- 从用户点击购买->生成内部订单
- 初始化库存:需要后端库存入Redis(jpa、redis)
- 锁用户(redis)
- 库存数量校验(redis)
- 锁库存(redis)
- 生成内部订单
- 等待订单生成(redis)
- 扣库存(redis)
- 释放库存锁(redis)
provider模块负责:
- 排队生成外部订单(activemq)
以下是详细代码:
web模块:
依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- dubbo begin springboot 1.5.9,对应dubbo 0.1.0 -->
<dependency>
<groupId>com.alibaba.boot</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>${dubbo.version}</version>
</dependency>
<!-- dubbo end -->
<!-- zkclient begin -->
<dependency>
<groupId>com.101tec</groupId>
<artifactId>zkclient</artifactId>
<version>${zkclient.version}</version>
</dependency>
<!-- zkclient end -->
<!-- api模块是consumer和provider公共依赖-->
<dependency>
<groupId>com.lcywings.sbt</groupId>
<artifactId>buy-limit-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<!-- api end -->
<!-- processor begin -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- processor end -->
配置:
# 应用名称
spring.application.name=buy-limit-web
# 端口号
server.port=8090
# dubbo+zookeeper配置
dubbo.application.name=edoc-consumer
# 注册中心地址
dubbo.registry.address=127.0.0.1:2181
dubbo.registry.protocol=zookeeper
dubbo.scan.base-packages=com.lcywings.sbt
# redis配置
spring.redis.host=127.0.0.1
spring.redis.port=6379
# 抢购业务自定义系统配置
# 商品库存初始数量,实际开发不会配置
buy.limit.prodStockNum=100
# 商品库存存放redis的标识前缀
buy.limit.stockNumPrefix=prod-stock-num-
#锁定用户的标识前缀
buy.limit.lockUerPrefix=lock-user-
#初始化抢购活动的时长
buy.limit.buyLimitTime=1800
#商品库存锁定标识前缀
buy.limit.lockProdStockPrefix=lock-prod-stock-
# 商品库存锁定时长
buy.limit.lockProdStockTime=60
# 生成订单超时时长
buy.limit.tradeOrderLimitTime=30
BuyLimitConfig
package com.lcywings.sbt.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* Created on 2021/8/12.
* <p>
* Author : Lcywings
* <p>
* Description : 抢购自动配置类
*/
@Component
@ConfigurationProperties(prefix = "buy.limit")
@Data
public class BuyLimitConfig {
/**
* 初始库存数量
*/
private Integer prodStockNum;
/**
* 库存数量的前缀
*/
private String stockNumPrefix;
/**
* 锁定用户的标识前缀
*/
private String lockUerPrefix;
/**
* 抢购活动的时长
*/
private Integer buyLimitTime;
/**
* 商品库存锁定标识前缀
*/
private String lockProdStockPrefix;
/**
* 商品库存锁定时长
*/
private Integer lockProdStockTime;
/**
* 生成订单超时时长
*/
private Integer tradeOrderLimitTime;
}
BuyLimitController
package com.lcywings.sbt.controller;
import com.alibaba.dubbo.config.annotation.Reference;
import com.lcywings.sbt.api.TradeOrderApi;
import com.lcywings.sbt.service.BuyLimitService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.UUID;
/**
* Created on 2021/8/12.
* <p>
* Author : Lcywings
* <p>
* Description : 抢购业务入口
*/
@RestController
@Slf4j
public class BuyLimitController {
@Autowired
private BuyLimitService buyLimitService;
@Reference
private TradeOrderApi tradeOrderApi;
/**
* @author : Lcywings
* @date : 2021/8/12 11:21
* @acl : true
* @description : 开始抢购入口
*/
@RequestMapping(value = "/buyLimit", method = {RequestMethod.GET, RequestMethod.POST})
public String buyLimit(@RequestParam String userId, @RequestParam String prodId, @RequestParam Integer buyCount) throws Exception {
log.info("------ 0 开始抢购,用户编号{},商品编号{},购买数量{} ------", userId, prodId, buyCount);
// 模拟初始化商品库存数量到redis中,实际开发过程中,是后台初始化到Redis
log.warn("------ 1 初始化商品{}的库存数量 ------", prodId);
if (!buyLimitService.initProdStorkToRedis(prodId)) {
log.warn("------ 1 初始化商品:{},库存数量失败或者是已经存在 ------", prodId);
}
// 锁定用户,抢购业务针对同一个用户,在当前商品整个抢购过程中,只能抢购一次
log.warn("------ 2 校验当前抢购用户:{},是否重复购买/是否成功抢购过改商品 ------", userId, prodId);
if (!buyLimitService.lockBuyLimitUser(userId, prodId)) {
log.warn("------ 2 同一个用户:{},抢购商品{},只能抢购一次 ------", userId, prodId);
return "抢购失败,同一个商品的抢购活动中,同一个用户只能抢购一次!";
}
// 校验库存,个人库存不足,直接返回抢购失败
log.warn("------ 3 校验当前抢购用户:{},抢购商品:{},库存数量是否充足 -------", prodId, buyCount);
if (!buyLimitService.checkProdStock(prodId, buyCount)) {
log.warn("------ 3 商品:{},购买数量:{},库存不足 -------", prodId, buyCount);
return "抢购失败,当前商品库存数量不足!";
}
// 锁库存,如果当前锁库存操作失败,说明库存已经被其他用户锁住了,必须等待释放库存锁
while (true) {
// 循环判断库存锁是不是已经被释放,只有释放了库存锁,才可以进行后续流程操作(为了防止库存锁释放一次,必须给库存锁加过期时长)
if (!buyLimitService.isProdStockLocked(prodId)) {
log.info("------ 4 锁定商品库存成功,用户编号:{},商品编号:{} ------", userId, prodId);
break;
}
}
// 模拟生成订单,省略实体参数
String tradeOrderNo = "T" + DateTimeFormatter.ofPattern("yyMMddHHmmss").format(LocalDateTime.now()) + UUID.randomUUID().toString().replace("-", "").substring(0, 6);
log.info("----- 5 开始生成订单,用户:{},商品:{},购买数量:{},订单号:{} ------", userId, prodId, buyCount, tradeOrderNo);
// 将当前订单信息存入redis
buyLimitService.setTradeOrderTORedis(userId, prodId, tradeOrderNo);
// 调用订单中心接口,生成订单,在订单接口信息
tradeOrderApi.createBuyLimitTradeOrder(userId, prodId, tradeOrderNo);
// 等待订单生成成功
while (true) {
// 判断订单是否生成成功
int createStatus = buyLimitService.getCreatedTradeOrderStatus(userId, prodId);
if (createStatus == 1) {
log.info("----- 6 订单生成成功,用户:{},商品:{},购买数量:{},订单号:{} ------", userId, prodId, buyCount, tradeOrderNo);
break;
} else if (createStatus == 2 || createStatus == 3) {
// 订单生成失败或者异常
log.warn("----- 7 订单生成失败,用户:{},商品:{},购买数量:{},订单号:{} ------", userId, prodId, buyCount, tradeOrderNo);
// 释放锁住用户
buyLimitService.unlockBuyLimitUser(userId, prodId);
// 释放库存锁(不用扣减库存)
buyLimitService.unlockProdStock(prodId);
return "抢购失败,请重新操作!";
}
}
// 订单生成成功,扣减库存
buyLimitService.subProdStockNum(prodId, buyCount);
// 释放库存锁
buyLimitService.unlockProdStock(prodId);
log.info("------ 抢购成功,用户编号:{},商品编号:{},购买数量:{} ------", userId, prodId, buyCount);
return "抢购成功!订单号为:" + tradeOrderNo;
}
}
BuyLimitService
package com.lcywings.sbt.service;
/**
* Created on 2021/8/12.
* <p>
* Author : Lcywings
* <p>
* Description : 抢购业务接口
*/
public interface BuyLimitService {
/**
* @author : Lcywings
* @date : 2021/8/12 11:24
* @acl : true
* @description : 初始化商品库存数量到redis
*/
boolean initProdStorkToRedis(String prodId);
/**
* @author : Lcywings
* @date : 2021/8/12 11:47
* @acl : true
* @description : 锁定商品的抢购用户
*/
boolean lockBuyLimitUser(String userId, String prodId);
/**
* @author : Lcywings
* @date : 2021/8/12 14:05
* @acl : true
* @description : 校验商品库存是否充足
*/
boolean checkProdStock(String prodId, Integer buyCount);
/**
* @author : Lcywings
* @date : 2021/8/12 14:13
* @acl : true
* @description : 校验库存追否已被锁定,没有就上锁,并增加超时时长
*/
boolean isProdStockLocked(String prodId);
/**
* @author : Lcywings
* @date : 2021/8/12 14:29
* @acl : true
* @description : 将订单信息存入redis
*/
void setTradeOrderTORedis(String userId, String prodId, String tradeOrderNo);
/**
* @author : Lcywings
* @date : 2021/8/13 9:14
* @acl : true
* @description : 家宴订单生成状态,0正在生成,1生成成功,3超时取消
*/
int getCreatedTradeOrderStatus(String userId, String prodId);
/**
* @author : Lcywings
* @date : 2021/8/13 9:41
* @acl : true
* @description : 释放锁住的用户
*/
void unlockBuyLimitUser(String userId, String prodId);
/**
* @author : Lcywings
* @date : 2021/8/13 9:44
* @acl : true
* @description : 释放库存锁
*/
void unlockProdStock(String prodId);
/**
* @author : Lcywings
* @date : 2021/8/13 9:48
* @acl : true
* @description : 抢购成功,扣减库存
*/
void subProdStockNum(String prodId, Integer buyCount);
}
BuyLimitServiceImpl
package com.lcywings.sbt.service.impl;
import com.lcywings.sbt.config.BuyLimitConfig;
import com.lcywings.sbt.service.BuyLimitService;
import com.lcywings.sbt.utils.RedisUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import java.util.HashMap;
import java.util.Map;
/**
* Created on 2021/8/12.
* <p>
* Author : Lcywings
* <p>
* Description : 抢购业务实现类
*/
@Service
@Slf4j
public class BuyLimitServiceImpl implements BuyLimitService {
@Autowired
private BuyLimitConfig buyLimitConfig;
@Autowired
private RedisUtils redisUtils;
@Override
public boolean initProdStorkToRedis(String prodId) {
// 如果redis中已经初始化过商品库存数量,不需要重复操作,如果没有才需要初始化
return redisUtils.get(buyLimitConfig.getStockNumPrefix() + prodId) == null
&& redisUtils.set(buyLimitConfig.getStockNumPrefix() + prodId, buyLimitConfig.getProdStockNum());
}
@Override
public boolean lockBuyLimitUser(String userId, String prodId) {
// 使用redis分布式锁,锁定当前商品的抢购用户,借助名称空间
return redisUtils.lock(buyLimitConfig.getLockUerPrefix() + prodId + ":" + userId, null, buyLimitConfig.getBuyLimitTime());
}
@Override
public boolean checkProdStock(String prodId, Integer buyCount) {
// 从redis获取商品库存数量
Object prodStockNum = redisUtils.get(buyLimitConfig.getStockNumPrefix() + prodId);
return prodStockNum != null && Integer.valueOf(prodStockNum.toString()) >= buyCount;
}
@Override
public boolean isProdStockLocked(String prodId) {
// 直接使用分布式锁,上锁,如果上锁失败,说明已经被锁定,如果上锁成功,没有被锁,可以进行抢购
return !redisUtils.lock(buyLimitConfig.getLockProdStockPrefix() + prodId, null, buyLimitConfig.getLockProdStockTime());
}
@Override
public void setTradeOrderTORedis(String userId, String prodId, String tradeOrderNo) {
// 使用用户编号作为唯一标识(方便前端根用户编号判断订单是否生成成功)
Map<String, Object> tradeOrderMap = new HashMap<>();
tradeOrderMap.put("tradeOrderNo ", tradeOrderNo);
// 订单状态为0,代表正在生成订单
tradeOrderMap.put("tradeOrderStatus", 0);
redisUtils.hmset(userId + "-" + prodId, tradeOrderMap, buyLimitConfig.getTradeOrderLimitTime());
}
@Override
public int getCreatedTradeOrderStatus(String userId, String prodId) {
// 判断订单是否超时(生成订单存入redis有个限制时间,超出时间自动删除),没有订单号,覆盖订单状态为超时取
if (!redisUtils.hasKey(userId + "-" + prodId)) {
redisUtils.hset(userId + "-" + prodId, "tradeOrderStatus", 3);
}
// 如果没有超时,获取订单状态(先判断redis中是否真的没有超时,防止上一步还没超时,到这一步超时)
if (!ObjectUtils.isEmpty(redisUtils.hget(userId + "-" + prodId, "tradeOrderStatus"))) {
return (int) redisUtils.hget(userId + "-" + prodId, "tradeOrderStatus");
}
// 返回订单正在生成标识,继续等待
return 0;
}
@Override
public void unlockBuyLimitUser(String userId, String prodId) {
// 删除锁用户
redisUtils.delLock(buyLimitConfig.getLockProdStockPrefix() + userId + ":" + prodId);
}
@Override
public void unlockProdStock(String prodId) {
// 删除锁库存
redisUtils.delLock(buyLimitConfig.getLockProdStockPrefix() + prodId);
}
@Override
public void subProdStockNum(String prodId, Integer buyCount) {
//抢购成功,减少库存数量
redisUtils.decr(buyLimitConfig.getStockNumPrefix() + prodId, buyCount);
}
}
provider模块:
依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- dubbo begin springboot 1.5.9,对应dubbo 0.1.0 -->
<dependency>
<groupId>com.alibaba.boot</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>${dubbo.version}</version>
</dependency>
<!-- dubbo end -->
<!-- zkclient begin -->
<dependency>
<groupId>com.101tec</groupId>
<artifactId>zkclient</artifactId>
<version>${zkclient.version}</version>
</dependency>
<!-- zkclient end -->
<!-- api模块是consumer和provider公共依赖-->
<dependency>
<groupId>com.lcywings.sbt</groupId>
<artifactId>buy-limit-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<!-- api end -->
<!-- activemq begin -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-activemq</artifactId>
</dependency>
<!-- activemq end -->
配置:
# 应用名称
spring.application.name=buy-limit-provider
# 端口号
server.port=8089
# dubbo+zookeeper配置
dubbo.application.name=edoc-provider
# 注册中心地址
dubbo.registry.address=127.0.0.1:2181
dubbo.registry.protocol=zookeeper
dubbo.protocol.port=-1
# activemq配置
spring.activemq.broker-url=tcp://localhost:61616
spring.activemq.user=admin
spring.activemq.password=admin
# 信任所有包,保证消息消息对象在传输过程中,能争取序列化成功
spring.activemq.packages.trust-all=true
# 默认情况下,ActiveMQ使用的模式是队列Queue,如果要是用主题队列模式Topic模式,必须改默认配置
# 如果单独开主题模式,会导致系统只能使用主题,不可以使用点对点模式
#spring.jms.pub-sub-domain=true
# redis配置
spring.redis.host=127.0.0.1
spring.redis.port=6379
TradeOrderProvider
package com.lcywings.sbt.provider;
import com.alibaba.dubbo.config.annotation.Service;
import com.lcywings.sbt.api.TradeOrderApi;
import lombok.extern.slf4j.Slf4j;
import org.apache.activemq.command.ActiveMQQueue;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jms.core.JmsMessagingTemplate;
import javax.jms.Destination;
import java.util.HashMap;
import java.util.Map;
/**
* Created on 2021/8/13.
* <p>
* Author : Lcywings
* <p>
* Description : 抢购西单服务提供者
*/
@Service //阿里的
@Slf4j
public class TradeOrderProvider implements TradeOrderApi {
@Autowired
private JmsMessagingTemplate jmsMessagingTemplate;
@Override
public void createBuyLimitTradeOrder(String userId, String prodId, String tradeOrderNo) throws Exception {
log.info("****** 收到生成订单请求,用户编号:{},商品编号:{},交易订单号:{}, ******", userId, prodId, tradeOrderNo);
// TODO 收到下单请求,必须要做参数校验,用户信息获取,商品信息获取,创建订单实体对象等待
log.info("****** 将生成订单请求发送到异步生成订单消息队列中 ******");
// 创建消息队列
Destination destination = new ActiveMQQueue("buy-limit-create-order-queue");
// 创建消息对象
Map<String, String> paramMap = new HashMap<>();
paramMap.put("userId", userId);
paramMap.put("prodId", prodId);
paramMap.put("tradeOrderNo", tradeOrderNo);
// 发送消息队列,注意:使用高可用的消息队列-主从或者集群,防止队列不可用或者消息丢失
jmsMessagingTemplate.convertAndSend(destination, paramMap);
}
}
TradeOrderListener
package com.lcywings.sbt.listener;
import com.lcywings.sbt.utils.RedisUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jms.annotation.JmsListener;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* Created on 2021/8/13.
* <p>
* Author : Lcywings
* <p>
* Description : 监听生成订单的消息队列,生成订单
*/
@Component
@Slf4j
public class TradeOrderListener {
@Autowired
private RedisUtils redisUtils;
/**
* @author : Lcywings
* @date : 2021/8/13 9:03
* @acl : true
* @description : 从生成抢购订单队列中,获取生成订单请求,开始生成订单
*/
@JmsListener(destination = "buy-limit-create-order-queue")
public void createTradeOrderFromQueue(Map<String, String> paramsMap) {
log.info("###### 开始生成订单,请求详情:{}", paramsMap);
// TODO 开始生成订单,存入数据库,存入缓存redis中等
// 生成订单成功,修改redis中抢购的订单的状态为1,生成失败或者异常,改为2
redisUtils.hset(paramsMap.get("userId") + "-" + paramsMap.get("prodId"), "tradeOrderStatus", 1);
}
}
Q.E.D.