【MQ】酒店项目实战 - 缓存一致性

313 阅读3分钟

「这是我参与2022首次更文挑战的第6天,活动详情查看:2022首次更文挑战

一、前言

学习内容:

  1. 多级缓存:JVM 进程内缓存、Redis 缓存(让缓存数据离用户足够近)
  2. 多级缓存存在的问题
  3. 多级缓存的数据一致性

(1)多级缓存

对比相应查询:

  • 传统查询:直接落数据库
  • 多级缓存查询JVM 缓存 -> Redis 缓存 -> 数据库

让缓存数据离用户足够近:

  • Redis 缓存:将数据从磁盘搬到内存,加快了数据的查询效率
  • JVM 缓存:让数据响应效率进一步提升。JVM 的缓存大小取决于 JVM 的大小,存放热点数据。

cache-多级缓存.png

那么相应的查询步骤:

  1. 先从 JVM 缓存中查询
  2. 查询 Redis 缓存
  3. 查询 数据库(查询成功后,写入 Redis,写入本地缓存)

多级缓存存在的问题:数据一致性

讨论一个问题:缓存的数据如何写入?

在写入缓存数据的时候如何保持 Redis 缓存和 JVM 本地缓存的一致性呢?

解决方案:使用消息中间件(RocketMQ)作为 RedisJVM缓存之间的通讯员。

当修改 Redis 缓存的同时发送 RocketMQ 广播消息,通知修改 JVM缓存。 消息格式:JSON

cache-消息通知.png

管理端操作:

这里有个小细节:这三个操作的事务一致性。

  1. 修改数据库中数据
  2. 更新设置 Redis 中数据
  3. RocketMQ 发送更新消息

消费端操作:

  1. 监听 RocketMQ 对应消息
  2. 获取相应数据:从 Redis 中获取,若获取不到,则从数据库中拉取
  3. 更新相应数据到 JVM 缓存



二、代码实现

这里代码分为:

  • 管理端
  • 消费端

(1)管理端

主要分为:

  1. 配置类
  2. 操作

配置类如下:

@Configuration
public class HotelRoomProducerConfiguration {

    @Value("${rocketmq.namesrv.address}")
    private String namesrvAddress;

    @Value("${rocketmq.hotelRoom.producer.group}")
    private String hotelRoomProducerGroup;

    @Bean(value = "hotelRoomMqProducer")
    public DefaultMQProducer hotelRoomMqProducer() throws MQClientException {
        DefaultMQProducer producer = new DefaultMQProducer(hotelRoomProducerGroup);
        producer.setNamesrvAddr(namesrvAddress);
        producer.start();
        return producer;
    }
}

对应监听器:

@Service
public class AdminRoomServiceImpl implements AdminRoomService {

    private Logger LOGGER = LoggerFactory.getLogger(AdminRoomServiceImpl.class);

    /**
     * mysql dubbo服务
     */
    @Reference(version = "1.0.0",
            interfaceClass = MysqlApi.class,
            cluster = "failfast")
    private MysqlApi mysqlApi;

    /**
     * redis dubbo服务
     */
    @Reference(version = "1.0.0",
            interfaceClass = RedisApi.class,
            cluster = "failfast")
    private RedisApi redisApi;

    /**
     * 房间管理mq producer
     */
    @Autowired
    @Qualifier(value = "hotelRoomMqProducer")
    private DefaultMQProducer hotelRoomMqProducer;

    /**
     * 酒店房间topic
     */
    @Value("${rocketmq.hotelRoom.topic}")
    private String hotelRoomTopic;
    /**
     * 修改酒店信息
     * 1. 更新数据库数据
     * 2. 更新设置 redis 数据
     * 3. 发送消息通知(RocketMQ)
     * 
     * @param hotelRoom 酒店信息
     * @return response
     */
    @Override
    public CommonResponse update(AdminHotelRoom hotelRoom) {
        String phoneNumber = hotelRoom.getPhoneNumber();
        MysqlRequestDTO mysqlRequestDTO = new MysqlRequestDTO();
        mysqlRequestDTO.setProjectTypeEnum(LittleProjectTypeEnum.ROCKETMQ);
        mysqlRequestDTO.setPhoneNumber(phoneNumber);
        mysqlRequestDTO.setSql("UPDATE t_shop_goods SET pcate = ?, title = ?, thumb_url = ?, productprice = ?, total = ?, totalcnf = ? WHERE id = ?");
        this.builderSqlParams(hotelRoom, mysqlRequestDTO);
        LOGGER.info("start update hotel room param:{}", 
                    JSON.toJSONString(mysqlRequestDTO));
        // 写mysql
        CommonResponse<Integer> response = mysqlApi.update(mysqlRequestDTO);
        LOGGER.info("end update hotel room param:{}, response:{}", JSON.toJSONString(mysqlRequestDTO), JSON.toJSONString(response));

        // db更新成功后,更新redis,发客房数据更新的消息
        if (Objects.equals(response.getCode(), ErrorCodeEnum.SUCCESS.getCode())) {
            Long roomId = hotelRoom.getId();

            // 查询更新后的数据
            AdminHotelRoom adminHotelRoom = this.getHotelRoomById(roomId, phoneNumber);

            LOGGER.info("update hotel room data success update redis cache key:{}", 
                        HOTEL_ROOM_KEY_PREFIX + roomId);
            // 写redis
            redisApi.set(HOTEL_ROOM_KEY_PREFIX + roomId, 
                         JSON.toJSONString(adminHotelRoom), phoneNumber,
                         LittleProjectTypeEnum.ROCKETMQ);

            // 发客房数据更新的消息
            this.sendRoomUpdateMessage(phoneNumber, roomId);

        }
        return response;
    }

    /**
     * 发客房数据更新的消息到mq中
     *
     * @param phoneNumber 手机号
     * @param roomId      房间id
     */
    private void sendRoomUpdateMessage(String phoneNumber, Long roomId) {
        // 发送广播消息到mq中
        // 提供给小程序的api模块来消费消息后从redis中获取消息更新本地jvm内存
        Message message = new Message();
        message.setTopic(hotelRoomTopic);
        AdminHotelRoomMessage adminHotelRoomMessage = new AdminHotelRoomMessage();
        adminHotelRoomMessage.setRoomId(roomId);
        adminHotelRoomMessage.setPhoneNumber(phoneNumber);

        message.setBody(JSON.toJSONString(adminHotelRoomMessage)
                        .getBytes(StandardCharsets.UTF_8));
        try {
            LOGGER.info("start send hotel room update  message, param:{}", roomId);
            SendResult sendResult = hotelRoomMqProducer.send(message);
            LOGGER.info("end send hotel room update  message, param:{}, 
                        sendResult:{}", roomId, JSON.toJSONString(sendResult));
        } catch (Exception e) {
            LOGGER.error("send login success notify message fail, error message:{}", e);
        }
    }
}

(2)消费端

主要分为:

  1. 配置类
  2. 监听器:监听对应消息

配置类如下:

@Configuration
public class HotelRoomConsumerConfiguration {

    /**
     * namesrv address
     */
    @Value("${rocketmq.namesrv.address}")
    private String namesrvAddress;

    /**
     * 酒店房间topic
     */
    @Value("${rocketmq.hotelRoom.topic}")
    private String hotelRoomTopic;

    /**
     * 酒店房间数据监听的consumer group
     */
    @Value("${rocketmq.hotelRoom.consumer.group}")
    private String hotelRoomConsumerGroup;

    /**
     * 酒店客房消息的consumer bean
     *
     * @return 酒店客房消息的consumer bean
     */
    @Bean(value = "hotelRoomConsumer")
    public DefaultMQPushConsumer hotelRoomConsumer(HotelRoomUpdateMessageListener hotelRoomUpdateMessageListener) throws MQClientException {
        DefaultMQPushConsumer consumer = 
            new DefaultMQPushConsumer(hotelRoomConsumerGroup);
        consumer.setNamesrvAddr(namesrvAddress);
        consumer.subscribe(hotelRoomTopic, "*");
        consumer.setMessageListener(hotelRoomUpdateMessageListener);
        consumer.start();
        return consumer;
    }
}
  1. 监听器
@Component
public class HotelRoomUpdateMessageListener implements MessageListenerConcurrently {

    private static final Logger LOGGER = 
        LoggerFactory.getLogger(HotelRoomUpdateMessageListener.class);

    @Reference(version = "1.0.0",
            interfaceClass = RedisApi.class,
            cluster = "failfast")
    private RedisApi redisApi;

    /**
     * 酒店房间本地缓存
     */
    @Autowired
    private HotelRoomCacheManager hotelRoomCacheManager;

    @Override
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
                                                    ConsumeConcurrentlyContext context) {
        // 处理房间更新成功的消息
        // 从redis中取更新更新房间缓存信息
        for (MessageExt msg : msgs) {
            // TODO 可以优化为批量查询
            String body = new String(msg.getBody(), StandardCharsets.UTF_8);
            try {
                HotelRoomMessage hotelRoomMessage = 
                    JSON.parseObject(body, HotelRoomMessage.class);
                Long roomId = hotelRoomMessage.getRoomId();
                LOGGER.info("receiver room update message roomId:{}", roomId);
                LOGGER.info("start query hotel room from redis cache param:{}", roomId);
                CommonResponse<String> commonResponse = 
                    redisApi.get(RedisKeyConstant.HOTEL_ROOM_KEY_PREFIX + roomId,
                                 hotelRoomMessage.getPhoneNumber(),
                                 LittleProjectTypeEnum.ROCKETMQ);
                LOGGER.info("end query hotel room from redis cache param:{}", roomId);
                if (Objects.equals(commonResponse.getCode(), 
                                   ErrorCodeEnum.SUCCESS.getCode())) {
                    // 成功 赋值给jvm内存
                    LOGGER.info("update hotel room local cache data:{}", 
                                commonResponse.getData());
                    hotelRoomCacheManager.updateLocalCache(
                        JSON.parseObject(commonResponse.getData(), HotelRoom.class));
                }
            } catch (Exception e) {
                // 消费失败
                LOGGER.info("received hotel room update message:{}, consumer fail", body);
                // Failure consumption,later try to consume 消费失败,以后尝试消费
                return ConsumeConcurrentlyStatus.RECONSUME_LATER;
            }
        }
        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
    }
}