分布式情况下本地缓存同步方案

前言

项目中使用了两级缓存但是由于使用策略不当,出现了本地缓存不一致的情况

两级缓存数据数据不一致性问题

伪代码:

    @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

  • 1
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值