「这是我参与2022首次更文挑战的第6天,活动详情查看:2022首次更文挑战」
一、前言
学习内容:
- 多级缓存:
JVM
进程内缓存、Redis
缓存(让缓存数据离用户足够近) - 多级缓存存在的问题
- 多级缓存的数据一致性
(1)多级缓存
对比相应查询:
- 传统查询:直接落数据库
- 多级缓存查询:
JVM
缓存 ->Redis
缓存 -> 数据库
让缓存数据离用户足够近:
Redis
缓存:将数据从磁盘搬到内存,加快了数据的查询效率JVM
缓存:让数据响应效率进一步提升。JVM
的缓存大小取决于JVM
的大小,存放热点数据。
那么相应的查询步骤:
- 先从
JVM
缓存中查询 - 查询
Redis
缓存 - 查询 数据库(查询成功后,写入
Redis
,写入本地缓存)
多级缓存存在的问题:数据一致性
讨论一个问题:缓存的数据如何写入?
在写入缓存数据的时候如何保持
Redis
缓存和JVM
本地缓存的一致性呢?
解决方案:使用消息中间件(RocketMQ
)作为 Redis
和 JVM
缓存之间的通讯员。
当修改
Redis
缓存的同时发送RocketMQ
广播消息,通知修改JVM
缓存。 消息格式:JSON
管理端操作:
这里有个小细节:这三个操作的事务一致性。
- 修改数据库中数据
- 更新设置
Redis
中数据 - 给
RocketMQ
发送更新消息
消费端操作:
- 监听
RocketMQ
对应消息 - 获取相应数据:从
Redis
中获取,若获取不到,则从数据库中拉取 - 更新相应数据到
JVM
缓存
二、代码实现
这里代码分为:
- 管理端
- 消费端
(1)管理端
主要分为:
- 配置类
- 操作
配置类如下:
@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)消费端
主要分为:
- 配置类
- 监听器:监听对应消息
配置类如下:
@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;
}
}
- 监听器
@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;
}
}