【SpringBoot整合系列】SpringBoot整合RabbitMQ业务应用Demo

业务要求

  • 用户提交后,生成订单,用户有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模拟保存商品记录

  1. 根据商品ID,查询商品记录,将商品记录添加到redis中
  2. 这里主要是保存商品ID和库存信息

5.2提交订单业务

  1. 提交购买商品的信息(商品ID,数量)、用户信息(用户ID)
  2. 根据商品ID查询商品信息,判断库存是否充足
  3. 若库存充足则计算总价,并生成订单号,然后插入一条订单信息,默认支付状态为0:未支付,并返回该条记录的自增主键
  4. 若订单记录保存成功,则向redis中添加该条订单记录,并修改redis中商品的库存信息(减去订单中的数量)
  5. 发送一条消息到支付队列中,给用户XX时间进行支付

5.3用户支付业务(模拟)

  1. 根据传入的订单ID查询订单记录
  2. 这里直接模拟用户支付成功了
  3. 将该订单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

  1. 监听死信队列,根据订单号获取订单信息,判断用户XX分钟是否支付,若已支付则直接应答,反之则恢复库存,更新订单状态
  2. 监听订单队列,根据订单号获取订单信息,修改订单状态,这里因为是已经支付才发出,所以直接修改为已支付,这里redis和数据库可以一起修改了
  3. 监听库存队列,根据订单号获取订单信息,根据订单中的商品ID获取商品信息,修改库存,这里因为是已经支付才发出,所以更新数据库的库存
  4. 监听短信队列,根据订单号获取订单信息,根据订单中的用户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);
    }
}
  • 23
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值