前言
项目中使用了两级缓存但是由于使用策略不当,出现了本地缓存不一致的情况
两级缓存数据数据不一致性问题
伪代码:
@Scheduled(cron = "0 0/3 * * * ? ")
public void cacheRoom() {
Long start = System.currentTimeMillis();
log.info("Cache room data, execute start :{}", start);
RLock lock = redissonClusterClient.getLock(LockConstants.CACHE_ROOM_LOCK);
boolean isLock;
try {
//尝试加锁
isLock = lock.tryLock(0, 30L, TimeUnit.SECONDS);
log.info("Cache room data - acquire the distributed lock:{}", isLock);
if (!isLock) {
return;
}
//1.数据库获取数据
try {
//2.设置内存缓存
//3.设置redis缓存
} catch (Exception e) {
log.error("Failed to update the room cache and the exception is :{}", e.getMessage());
}
} catch (Exception e) {
log.error("获取分布锁 {} 时: {} 出现异常", LockConstants.CACHE_ROOM_LOCK, e.getMessage());
} finally {
try {
lock.unlock();
} catch (Exception e) {
log.warn("Cache room data - unlock the distributed lock:{}", e.getMessage());
}
log.info("Cache room data...... {}", LocalDateTime.now());
}
}
现象
定时任务触发是靠分布式锁控制,导致部分节点一直是老数据
思路
https://cloud.tencent.com/developer/article/1407568
考虑用redis订阅或者mq广播模式
解决方案
用的redisTemplate实现发布订阅
- 业务类
Map<String, Object> roomMap = new HashMap<>();
//数据库获取的数据
roomMap.put(roomKey, roomList);
//发布订阅更新内存缓存
redisTemplate.convertAndSend(RedisPatternTopic.CACHE_PATTERN,roomMap);
//更新redis
redisUtil.hmset(roomKey, roomMap, 600L);
- 配置类
@Configuration
public class MyRedisConf {
@Bean(name = "myRedisTemplate")
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 使用Jackson2JsonRedisSerialize 替换默认的jdkSerializeable序列化
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
// 设置value的序列化规则和 key的序列化规则
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
@Bean
public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory,
MessageListenerAdapter listenerAdapter) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.addMessageListener(listenerAdapter, new PatternTopic(RedisPatternTopic.CACHE_PATTERN));
return container;
}
/**
* 绑定消息监听者和接收监听的方法,必须要注入这个监听器,不然会报错
*/
@Bean
public MessageListenerAdapter listenerAdapter() {
//第一个参数 消费者对象 第二个参数 消费者方法
return new MessageListenerAdapter(new RedisReceiver(), "receiveMessage");
}
}
- 订阅频道
/**
* @Auther: wxy
* @Date: 2020/8/27 17:55
* @Description: redis订阅模式,订阅频道常量
*/
public interface RedisPatternTopic {
/**
* 同步内存缓存频道
*/
String CACHE_PATTERN = "cache_pattern";
}
- 消费者
/**
* @Auther: wxy
* @Date: 2020/8/27 17:14
* @Description:
*/
@Slf4j
@Component
public class RedisReceiver {
public void receiveMessage(String message) {
//序列化对象(特别注意:发布的时候需要设置序列化;订阅方也需要设置序列化)
Jackson2JsonRedisSerializer seria = new Jackson2JsonRedisSerializer(Map.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
seria.setObjectMapper(objectMapper);
//消息体
Map map = (Map) seria.deserialize(message.getBytes());
if (!CollectionUtils.isEmpty(map)) {
map.forEach(this::doParseMessage);
}
}
/**
* @param o1 key
* @param o2 value
*/
private void doParseMessage(Object o1, Object o2) {
String key = (String) o1;
switch (key) {
case RecommendCacheConstant.ROOM_LIST_L1:
if (o2 != null) {
List<Room> roomList = (List<Room>) o2;
//更新本地缓存
CacheRegistry.getInstance().put(key, roomList);
log.info("缓存订阅频道更新缓存key:{}",key);
}
break;
default:
log.info("RedisReceiver不支持消息类型key:{}", key);
}
}
}
结果
可以保证多节点同时刷新内存缓存,
- 优点:
实现简单:基于广泛使用的Redis,没有引入其他组件,而且实现逻辑也很简单
- 缺点:
在一些极端情况下,会出现缓存的更新不及时。比如模型更新后,收到请求的进程本地更新后返回结果,因为消息是异步的,可能还没达到Redis时,进程就挂掉了。
当模型更新时,各个进程中缓存的模型在很短的时间内存在不一致的情况。 会影响部分用户。不过这种情况是完全可以接受的。
- 注意事项
因为所有节点都订阅了同一频道,也会接听到自身广播的事件,所以节点在响应事件时,可以做幂等处理
其他
rabbimq的订阅模式也可以实现,订阅模式搭建
https://blog.csdn.net/weixin_43866295/article/details/86703757