目录
业务要求
- 用户提交后,生成订单,用户有XX分钟时间完成支付
- 如果用户XX分钟内完成支付,则更新订单为已支付,仓库扣库存,发送短信
- 如果用户XX分钟后未完成支付,则更新订单为未支付,仓库恢复库存
- 技术栈:springboot+redis+rabbitmq+mysql
代码实现
1.依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
2.配置
server:
port: 9090
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/mq?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8
driver-class-name: com.mysql.jdbc.Driver
username: root
password: 123456
type: com.zaxxer.hikari.HikariDataSource
redis:
host: 127.0.0.1
port: 6379
password: 123456
timeout: 3000ms
database: 0
rabbitmq:
host: 192.168.29.200
port: 5672
username: admin
password: admin
virtual-host: /
publisher-confirm-type: correlated # ??????????????
publisher-returns: true #???????????
listener:
simple:
acknowledge-mode: manual # ????
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: cn.eat.model
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
3.配置类
@Configuration
public class RabbitConfig {
@Bean
public Queue payQueue() {
//设置消息过期时间
Map<String, Object> args = new HashMap<>();
//2分钟
args.put("x-message-ttl", 2 * 60 * 1000);
//设置死信交换机
args.put("x-dead-letter-exchange", Constants.DLX_EXCHANGE);
//设置死信 routing_key
args.put("x-dead-letter-routing-key", Constants.DLX_ROUTING_KEY);
return new Queue(Constants.PAY_QUEUE, true, false, false, args);
}
@Bean
public DirectExchange payExchange() {
return new DirectExchange(Constants.PAY_EXCHANGE, true, false);
}
@Bean
public Binding payBinding() {
return BindingBuilder.bind(payQueue()).
to(payExchange()).
with(Constants.PAY_ROUTING_KEY);
}
@Bean
public Queue dlxQueue() {
return new Queue(Constants.DLX_QUUE);
}
@Bean
public DirectExchange dlxExchange() {
return new DirectExchange(Constants.DLX_EXCHANGE, true, false);
}
@Bean
public Binding dlxBinding() {
return BindingBuilder.bind(dlxQueue()).
to(dlxExchange()).
with(Constants.DLX_ROUTING_KEY);
}
@Bean
public Queue orderQueue() {
return new Queue(Constants.ORDER_QUEUE);
}
@Bean
public Queue stockQueue() {
return new Queue(Constants.STOCK_QUEUE);
}
@Bean
public Queue phoneQueue() {
return new Queue(Constants.PHONE_QUEUE);
}
@Bean
public FanoutExchange orderExchange() {
return new FanoutExchange(Constants.ORDER_EXCHANGE, true, false);
}
@Bean
public Binding bindingOrder() {
return BindingBuilder.bind(orderQueue()).to(orderExchange());
}
@Bean
public Binding bindingStock() {
return BindingBuilder.bind(stockQueue()).to(orderExchange());
}
@Bean
public Binding bindingPhone() {
return BindingBuilder.bind(phoneQueue()).to(orderExchange());
}
}
4.接口Controller
- 模拟添加商品到redis,目的提高查询效率
- 提交订单,用户有XX分钟可以完成支付
- 用户主动支付,来完成订单
@RestController
public class EatController {
@Resource
private EatService eatService;
/**
* 添加商品数据到redis,模拟商品库存
* @param id
* @return
* @throws JsonProcessingException
*/
@RequestMapping("/setRedis")
public String setRedis(Integer id) throws JsonProcessingException {
return eatService.addRedis(id);
}
/**
* 提交订单
* @param order
* @return
* @throws JsonProcessingException
*/
@RequestMapping("/submit")
public String submit(Order order ) throws JsonProcessingException {
return eatService.submit(order);
}
/**
* 用户主动支付
* @param orderId
* @return
* @throws JsonProcessingException
*/
@RequestMapping("/topay")
public String toPay(Integer orderId) throws JsonProcessingException {
return eatService.toPay(orderId);
}
/*@RequestMapping("/cancel")
public String cancel(Integer orderId) throws JsonProcessingException {
return eatService.cancel(orderId);
}*/
}
5.业务Service
5.1模拟保存商品记录
- 根据商品ID,查询商品记录,将商品记录添加到redis中
- 这里主要是保存商品ID和库存信息
5.2提交订单业务
- 提交购买商品的信息(商品ID,数量)、用户信息(用户ID)
- 根据商品ID查询商品信息,判断库存是否充足
- 若库存充足则计算总价,并生成订单号,然后插入一条订单信息,默认支付状态为0:未支付,并返回该条记录的自增主键
- 若订单记录保存成功,则向redis中添加该条订单记录,并修改redis中商品的库存信息(减去订单中的数量)
- 发送一条消息到支付队列中,给用户XX时间进行支付
5.3用户支付业务(模拟)
- 根据传入的订单ID查询订单记录
- 这里直接模拟用户支付成功了
- 将该订单ID作为消息消息到扇形交换机,扇出到订单、库存、短信三个队列中
完整代码
@Service
public class EatService {
@Resource
private EatMapper eatMapper;
@Resource
private ObjectMapper objectMapper;
@Resource
private RabbitTemplate rabbitTemplate;
public String addRedis(Integer id) throws JsonProcessingException {
Goods goods = eatMapper.selectGoodsById(id);
if (goods != null){
//添加redis库存
RedisStringUtil.set(Constants.GOODS_PREFIX + id, objectMapper.writeValueAsString(goods));
return "success:数据缓存成功";
}
return "fail";
}
/**
* 用户提交订单
* @param order
* @return
* @throws JsonProcessingException
*/
public String submit(Order order) throws JsonProcessingException {
//修改redis库存
String goodsJson = RedisStringUtil.get(Constants.GOODS_PREFIX + order.getGoodsId());
Goods goods;
if (goodsJson == null){
goods = eatMapper.selectGoodsById(order.getGoodsId());
}else {
goods = objectMapper.readValue(goodsJson, Goods.class);
}
if(goods.getStock() < 1){
return "fail:库存不足";
}
order.setAmount(order.getNum() * goods.getPrice());
order.setOrderNo(new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date()) + order.getGoodsId() + order.getUserId());
int line = eatMapper.insertOrder(order);
if (line > 0){
//订单添加成功
RedisStringUtil.set(Constants.ORDER_PREFIX + order.getGoodsId(), objectMapper.writeValueAsString(order));
goods.setStock(goods.getStock() - order.getNum());
RedisStringUtil.set(Constants.GOODS_PREFIX + order.getGoodsId(), objectMapper.writeValueAsString(goods));
//延迟消息
rabbitTemplate.convertAndSend(Constants.PAY_EXCHANGE,Constants.PAY_ROUTING_KEY,order.getId());
return "success:请于三十分钟内完成支付";
}
return "fail";
}
/**
* 用户支付
* @param orderId
* @return
* @throws JsonProcessingException
*/
public String toPay(Integer orderId) throws JsonProcessingException {
String orderJson = RedisStringUtil.get(Constants.ORDER_PREFIX + orderId);
Order order;
if (orderJson != null){
order = objectMapper.readValue(orderJson, Order.class);
}else {
order = eatMapper.selectOrderById(orderId);
}
if (order.getStatus() == 1){
return "fail:订单已支付";
}else if (order.getStatus() == 2){
return "fail:订单已失效";
}
//模拟用户支付中,并且支付成功
//发送消息
rabbitTemplate.convertAndSend(Constants.ORDER_EXCHANGE,null,order.getId());
return "success:支付成功";
}
//用户取消支付
/*public String cancel(Integer orderId) {
return "success:取消支付";
}*/
}
6.消息消费者Consumer
- 监听死信队列,根据订单号获取订单信息,判断用户XX分钟是否支付,若已支付则直接应答,反之则恢复库存,更新订单状态
- 监听订单队列,根据订单号获取订单信息,修改订单状态,这里因为是已经支付才发出,所以直接修改为已支付,这里redis和数据库可以一起修改了
- 监听库存队列,根据订单号获取订单信息,根据订单中的商品ID获取商品信息,修改库存,这里因为是已经支付才发出,所以更新数据库的库存
- 监听短信队列,根据订单号获取订单信息,根据订单中的用户ID获取用户信息,模拟给用户发送短信,监听库存队列,修改库存,这里因为是已经支付才发出,所以直接发送
完整代码
@Component
@Slf4j
public class ReceiverLinstener {
@Resource
private ObjectMapper objectMapper;
@Resource
private EatMapper eatMapper;
/**
* 监听死信队列,判断用户XX分钟是否支付,
* 若已支付则直接应答,反之则恢复库存,更新订单状态
* @param message
* @param channel
* @throws IOException
*/
@RabbitListener(queues = Constants.DLX_QUUE)
public void receivePay(Message message, Channel channel) throws IOException {
Order order = getOrder(String.valueOf(message.getPayload()));
if(order.getStatus() == 0){
log.info("订单未支付,恢复库存");
//订单未支付
//修改订单状态
order.setStatus(2);
eatMapper.updateOrder(order);
RedisStringUtil.set(Constants.ORDER_PREFIX + order.getId(),objectMapper.writeValueAsString(order));
//恢复库存
String goodsJson = RedisStringUtil.get(Constants.GOODS_PREFIX + order.getGoodsId());
if (goodsJson != null){
Goods goods = objectMapper.readValue(goodsJson, Goods.class);
goods.setStock(goods.getStock() + order.getNum());
RedisStringUtil.set(Constants.GOODS_PREFIX + order.getGoodsId(),objectMapper.writeValueAsString(goods));
}
}else {
log.info("订单已支付,无需恢复库存");
}
channel.basicAck(((Long) message.getHeaders().get(AmqpHeaders.DELIVERY_TAG)),true);
}
/**
* 监听订单队列,修改订单状态,这里因为是已经支付才发出,所以直接修改为已支付
* @param message
* @param channel
* @throws IOException
*/
@RabbitListener(queues = Constants.ORDER_QUEUE)
public void receiveOrder(Message message, Channel channel) throws IOException {
log.info("订单支付成功,修改订单状态");
Order order = getOrder(String.valueOf(message.getPayload()));
order.setStatus(1);
eatMapper.updateOrder(order);
channel.basicAck(((Long) message.getHeaders().get(AmqpHeaders.DELIVERY_TAG)),true);
}
/**
* 监听库存队列,修改库存,这里因为是已经支付才发出,所以更新数据库的库存
* @param message
* @param channel
* @throws IOException
*/
@RabbitListener(queues = Constants.STOCK_QUEUE)
public void receiveStock(Message message, Channel channel) throws IOException {
log.info("订单支付成功,修改库存");
Order order = getOrder(String.valueOf(message.getPayload()));
Goods goods = eatMapper.selectGoodsById(order.getGoodsId());
goods.setStock(goods.getStock() - order.getNum());
eatMapper.updateGoods(goods);
channel.basicAck(((Long) message.getHeaders().get(AmqpHeaders.DELIVERY_TAG)),true);
}
/**
* 监听短信队列,模拟给用户发送短信,监听库存队列,修改库存,这里因为是已经支付才发出,所以直接发送
* @param message
* @param channel
* @throws IOException
*/
@RabbitListener(queues = Constants.PHONE_QUEUE)
public void receivePhone(Message message, Channel channel) throws IOException {
Order order = getOrder(String.valueOf(message.getPayload()));
log.info("模拟给{}用户发送短信成功:",order.getUserId());
channel.basicAck(((Long) message.getHeaders().get(AmqpHeaders.DELIVERY_TAG)),true);
}
/**
* 通用:获取订单
* @param orderId
* @return
* @throws JsonProcessingException
*/
private Order getOrder(String orderId) throws JsonProcessingException {
Order order;
String orderJson = RedisStringUtil.get(Constants.ORDER_PREFIX + orderId);
if (orderJson != null) {
order = objectMapper.readValue(orderJson, Order.class);
}else {
order = eatMapper.selectOrderById(Integer.valueOf(orderId));
}
return order;
}
}
7.其他mapper、model、工具类
mapper
public interface EatMapper {
@Insert("insert into eat_order(userid,orderno,goodsid,num,amount,status) values(#{userId},#{orderNo},#{goodsId},#{num},#{amount},0)")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insertOrder(Order order);
@Select("select * from goods where id=#{id}")
Goods selectGoodsById(Integer id);
@Select("select * from eat_order where id=#{orderId}")
Order selectOrderById(Integer orderId);
@Update("update eat_order set status=#{status} where id=#{id}")
void updateOrder(Order order);
@Update("update goods set stock=#{stock} where id=#{id}")
void updateGoods(Goods goods);
}
注意:启动类上别忘了加Mapper扫描
@SpringBootApplication
@MapperScan("cn.eat.mapper")
public class EatingApplication {
public static void main(String[] args) {
SpringApplication.run(EatingApplication.class, args);
}
}
model
@Data
public class Goods {
private int id;
private String goodsName;
private double price;
private int stock;
}
@Data
public class Order {
private int id;
private int userId;
private String orderNo;
private int goodsId;
private int num;
private double amount;
private int status;
}
工具
Constants
public class Constants {
public static final String GOODS_PREFIX = "goods:";
public static final String ORDER_PREFIX = "order:";
public static final String PAY_QUEUE = "pay_queue";
public static final String PAY_EXCHANGE = "pay_exchange";
public static final String PAY_ROUTING_KEY = "pay_routing_key";
public static final String DLX_QUUE = "dlx_queue";
public static final String DLX_EXCHANGE = "dlx_exchange";
public static final String DLX_ROUTING_KEY = "dlx_routing_key";
public static final String ORDER_QUEUE = "order_queue";
public static final String STOCK_QUEUE = "stock_queue";
public static final String PHONE_QUEUE = "phone_queue";
public static final String ORDER_EXCHANGE = "order_exchange";
}
RedisKeyUtil
@Component
public class RedisKeyUtil {
private static StringRedisTemplate redisTemplate;
@Autowired
public void setRedisTemplate(StringRedisTemplate redisTemplate) {
RedisKeyUtil.redisTemplate = redisTemplate;
}
/** -------------------key相关操作--------------------- */
/**
* 删除key
*
* @param key
*/
public static void delete(String key) {
redisTemplate.delete(key);
}
/**
* 批量删除key
*
* @param keys
*/
public static void delete(Collection<String> keys) {
redisTemplate.delete(keys);
}
/**
* 序列化key
*
* @param key
* @return
*/
public static byte[] dump(String key) {
return redisTemplate.dump(key);
}
/**
* 是否存在key
*
* @param key
* @return
*/
public static Boolean hasKey(String key) {
return redisTemplate.hasKey(key);
}
/**
* 设置过期时间
*
* @param key
* @param timeout
* @param unit
* @return
*/
public static Boolean expire(String key, long timeout, TimeUnit unit) {
return redisTemplate.expire(key, timeout, unit);
}
/**
* 设置过期时间
*
* @param key
* @param date
* @return
*/
public static Boolean expireAt(String key, Date date) {
return redisTemplate.expireAt(key, date);
}
/**
* 查找匹配的key
*
* @param pattern
* @return
*/
public static Set<String> keys(String pattern) {
return redisTemplate.keys(pattern);
}
/**
* 将当前数据库的 key 移动到给定的数据库 db 当中
*
* @param key
* @param dbIndex
* @return
*/
public static Boolean move(String key, int dbIndex) {
return redisTemplate.move(key, dbIndex);
}
/**
* 移除 key 的过期时间,key 将持久保持
*
* @param key
* @return
*/
public static Boolean persist(String key) {
return redisTemplate.persist(key);
}
/**
* 返回 key 的剩余的过期时间
*
* @param key
* @param unit
* @return
*/
public static Long getExpire(String key, TimeUnit unit) {
return redisTemplate.getExpire(key, unit);
}
/**
* 返回 key 的剩余的过期时间
*
* @param key
* @return
*/
public static Long getExpire(String key) {
return redisTemplate.getExpire(key);
}
/**
* 从当前数据库中随机返回一个 key
*
* @return
*/
public static String randomKey() {
return redisTemplate.randomKey();
}
/**
* 修改 key 的名称
*
* @param oldKey
* @param newKey
*/
public static void rename(String oldKey, String newKey) {
redisTemplate.rename(oldKey, newKey);
}
/**
* 仅当 newkey 不存在时,将 oldKey 改名为 newkey
*
* @param oldKey
* @param newKey
* @return
*/
public static Boolean renameIfAbsent(String oldKey, String newKey) {
return redisTemplate.renameIfAbsent(oldKey, newKey);
}
/**
* 返回 key 所储存的值的类型
*
* @param key
* @return
*/
public static DataType type(String key) {
return redisTemplate.type(key);
}
}
RedisStringUtil
@Component
public class RedisStringUtil {
private static StringRedisTemplate redisTemplate;
@Autowired
public void setRedisTemplate(StringRedisTemplate redisTemplate) {
RedisStringUtil.redisTemplate = redisTemplate;
}
/** -------------------string相关操作--------------------- */
/**
* 设置指定 key 的值
*
* @param key
* @param value
*/
public static void set(String key, String value) {
redisTemplate.opsForValue().set(key, value);
}
/**
* 获取指定 key 的值
*
* @param key
* @return
*/
public static String get(String key) {
return redisTemplate.opsForValue().get(key);
}
/**
* 返回 key 中字符串值的子字符
*
* @param key
* @param start
* @param end
* @return
*/
public static String getRange(String key, long start, long end) {
return redisTemplate.opsForValue().get(key, start, end);
}
/**
* 将给定 key 的值设为 value ,并返回 key 的旧值(old value)
*
* @param key
* @param value
* @return
*/
public static String getAndSet(String key, String value) {
return redisTemplate.opsForValue().getAndSet(key, value);
}
/**
* 对 key 所储存的字符串值,获取指定偏移量上的位(bit)
* @param key
* @param offset
* @return
*/
public static Boolean getBit(String key, long offset) {
return redisTemplate.opsForValue().getBit(key, offset);
}
/**
* 批量获取
*
* @param keys
* @return
*/
public static List<String> multiGet(Collection<String> keys) {
return redisTemplate.opsForValue().multiGet(keys);
}
/**
* 设置ASCII码, 字符串'a'的ASCII码是97, 转为二进制是'01100001', 此方法是将二进制第offset位值变为value
*
* @param key 位置
* @param value 值,true为1, false为0
* @return
*/
public static boolean setBit(String key, long offset, boolean value) {
return redisTemplate.opsForValue().setBit(key, offset, value);
}
/**
* 将值 value 关联到 key ,并将 key 的过期时间设为 timeout
*
* @param key
* @param value
* @param timeout 过期时间
* @param unit 时间单位, 天:TimeUnit.DAYS 小时:TimeUnit.HOURS 分钟:TimeUnit.MINUTES
* 秒:TimeUnit.SECONDS 毫秒:TimeUnit.MILLISECONDS
*/
public static void setEx(String key, String value, long timeout, TimeUnit unit) {
redisTemplate.opsForValue().set(key, value, timeout, unit);
}
/**
* 只有在 key 不存在时设置 key 的值
*
* @param key
* @param value
* @return 之前已经存在返回false, 不存在返回true
*/
public static boolean setIfAbsent(String key, String value) {
return redisTemplate.opsForValue().setIfAbsent(key, value);
}
/**
* 用 value 参数覆写给定 key 所储存的字符串值,从偏移量 offset 开始
*
* @param key
* @param value
* @param offset 从指定位置开始覆写
*/
public static void setRange(String key, String value, long offset) {
redisTemplate.opsForValue().set(key, value, offset);
}
/**
* 获取字符串的长度
*
* @param key
* @return
*/
public static Long size(String key) {
return redisTemplate.opsForValue().size(key);
}
/**
* 批量添加
*
* @param maps
*/
public static void multiSet(Map<String, String> maps) {
redisTemplate.opsForValue().multiSet(maps);
}
/**
* 同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在
*
* @param maps
* @return 之前已经存在返回false, 不存在返回true
*/
public static boolean multiSetIfAbsent(Map<String, String> maps) {
return redisTemplate.opsForValue().multiSetIfAbsent(maps);
}
/**
* 增加(自增长), 负数则为自减
*
* @param key
* @return
*/
public static Long incrBy(String key, long increment) {
return redisTemplate.opsForValue().increment(key, increment);
}
/**
* @param key
* @return
*/
public static Double incrByFloat(String key, double increment) {
return redisTemplate.opsForValue().increment(key, increment);
}
/**
* 追加到末尾
*
* @param key
* @param value
* @return
*/
public static Integer append(String key, String value) {
return redisTemplate.opsForValue().append(key, value);
}
}