使用事件+阻塞式有序队列,解决分布式架构中Mysql与Redis缓存一致性问题。

295 阅读11分钟

前情提要:我还是在忙项目组件化,这次负责的是协议模块,里面的协议计算也挺有意思,虽然没采用规则引擎,不过这次主要分享的是协议结果的缓存更新方案,计算部分,这次可能不会过多讲,合在一起篇幅过长,感兴趣的也可以评论区催更,我再找时间写一章协议计算。

协议模块:规则分为航司规则,标准任务规则,特例任务规则,以下是它们的关系。

协议规则关系.png

对于这几个协议规则你可以理解为,航司规则配置的任务是航司跟机场购买的服务,该航司下的子航司下的航班匹配到的标准任务只能是航司任务下的,所以取的是交集数据,而特例规则是用来处理应急的,比如航司3U下有很多子航司,对于B1633这个子航司,今天不想做任务A,就添加一个有效期是今天的规则,匹配该子航司的特例规则,指定任务A不做,也可以额外指定任务F,多做一个航司任务之外的任务,来满足当前的机场业务需要。

协议计算+缓存同步方案流程图

2024-09-22-17-07-09-image.png

从有序队列获取同步信息,进行缓存同步。

2024-09-22-17-08-22-image.png

流程解析:协议计算是一个定时任务,执行机制是 @Scheduled(fixedDelay = 10000) 间隔10秒执行一次,上一次没执行完成就延后执行

数据一致性的要点:

  • 保证同一时刻只有一个生产者在产生数据, 同一时刻只有一个消费者在消费数据,有序队列已经在控制处理的数据处理的顺序了。
  • 保证发生同步事件跟协议变更数据录入数据库是同一事务,避免数据库录入失败,但发送了同步事件。
  • 在同步缓存方法中做异常兜底方法,出现异常就清空protocolEventQueue队列,并且删除redis和本地缓存,避免数据不一致。
  • 了解数据变化,避免潜在的脏数据遗留问题。

缓存同步方案代码:

redis缓存结构:Map<String,List> key是日期,value是该日期的协议数据列表

阻塞式队列

@Component public class ProtocolSyncService {

@Autowired
private RedissonClient redissonClient;

private RBlockingQueue<String> protocolEventQueue;


@PostConstruct
public void init() {

    // 初始化队列
    this.protocolEventQueue = redissonClient.getBlockingQueue(RedisConstant.REDIS_PROTOCOL_EVENT_QUEUE, JsonJacksonCodec.INSTANCE);

}

public void addToQueue(String event) {
        protocolEventQueue.add(event);
}

public String take() throws InterruptedException {
    return protocolEventQueue.take();
}

public BlockingQueue<String> getQueue() {
    return p

发送事件

        // 忽略协议计算过程
        // allProtocols 是计算的结果
        final List<Protocol> finalProtocols = new ArrayList<>(allProtocols);
        if (CollUtil.isNotEmpty(allProtocols)) {

            // 清理保障日期变更的脏数据
            List<Protocol> dirtyData = updateData.stream()
                    .filter(f -> Objects.nonNull(f.getOldSafeguardDate()))
                    .map(p -> {
                        p.setSafeguardDate(p.getOldSafeguardDate())
                                .setUpdateStatus(UpdateStatusEnum.DELETE.getCode());
                        return p;
                    })
                    .collect(Collectors.toList());

            allProtocols.addAll(dirtyData);

            Map<String, List<Protocol>> dateListMap = allProtocols.stream()
                    .collect(Collectors.groupingBy(protocol -> DateUtils.format(protocol.getSafeguardDate(), DateUtils.YYYY_MM_DD)));

            log.info("发送同步协议事件:{}", dateListMap.size());
            applicationContext.publishEvent(new SyncProtocolEvent(this, dateListMap));
        }

事件实体

/**
 * 同步协议事件
 *
 * @author fdh
 * @date 2024/7/31 15:23
 */
@ToString
@Getter
@Setter
public class SyncProtocolEvent extends ApplicationEvent implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long uniqueId;

    private Map<String, List<Protocol>> protocolMap;

    public SyncProtocolEvent() {
        super(new Object());
        this.uniqueId = IdUtil.getSnowflakeNextId();
    }

    public SyncProtocolEvent(Object source) {
        super(source);
        this.uniqueId = IdUtil.getSnowflakeNextId();
    }


    public SyncProtocolEvent(Object source, Map<String, List<Protocol>> protocolMap) {
        super(source);
        this.protocolMap = protocolMap;
        this.uniqueId = IdUtil.getSnowflakeNextId();
    }
}

事件监听器

    @EventListener(SyncProtocolEvent.class)
    public void handleSyncProtocolEvent(SyncProtocolEvent event) {

        log.info("同步protocol数据事件填装进有序队列:{}", event.getUniqueId());

        protocolSyncService.addToQueue(JSONUtil.toJsonStr(event));
    }

initRunner(监听阻塞式有序队列)

@Slf4j @Component(value = "protocolDataInitRunner") public class InitRunner implements CommandLineRunner {
@Autowired
private ProtocolService protocolService;

@Autowired
private RedissonClient redissonClient;

@Autowired
private ProtocolSyncService protocolSyncService;

private final ObjectMapper objectMapper;

private static final int BATCH_SIZE = 100; // 每次批处理的数量
private static final int POLL_TIMEOUT_MS = 5000; // 获取元素的超时时间

private final ExecutorService executor = Executors.newSingleThreadExecutor();

private final Lock processBatchLock = new ReentrantLock();

public InitRunner() {
    this.objectMapper = new ObjectMapper();
    this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}

@Override
public void run(String... args) throws Exception {
    startProcessing();
}

    public void startProcessing() {
    executor.execute(() -> {
        while (true) {
            try {

                log.info("检测队列是否阻塞");

                // 首先使用take()等待获取到第一个元素
                String initialElement = protocolSyncService.take();

                log.info("获取同步协议数据");
                List<String> batchStrList = new LinkedList<>();
                if (initialElement != null) {

                    long s1 = System.currentTimeMillis();

                    batchStrList.add(initialElement);

                    // 合并多个事件,避免消费速度赶不上生产速度,导致消息堆积 (在配置的等待时间内收集批处理的事件,合并处理,目前单条数据消费耗时600ms,比生产数据的间隔快的多了,所以没验证是否真实有效,参考代码的时候需要批判性的验证一下)
//                        long startTime = System.currentTimeMillis();
//                        boolean batchFull = false;
//
//                        while (!batchFull) {
//                            long elapsedTime = System.currentTimeMillis() - startTime;
//
//                            // 获取当前时间
//                            long waitTime = POLL_TIMEOUT_MS - elapsedTime;
//
//                            // 如果等待时间超过最大超时时间,强制退出
//                            if (waitTime <= 0) {
//                                break;
//                            }
//
//                            // 尝试从队列中批量获取数据
//                            int currentSize = protocolSyncService.getQueue().drainTo(batchStrList, BATCH_SIZE - 1);  // 减去1因为已经有一个元素了
//
//                            // 如果获取的数据量达到批处理大小,标记为满批
//                            if (currentSize >= BATCH_SIZE - 1) {
//                                batchFull = true;
//                            } else {
//
//                                // 阻塞直到新数据到达或超时
//                                protocolSyncService.getQueue().poll(waitTime, TimeUnit.MILLISECONDS);
//
//                                // 继续获取更多数据
//                                int additionalSize = protocolSyncService.getQueue().drainTo(batchStrList, BATCH_SIZE - 1 - currentSize);
//                                if (additionalSize > 0) {
//                                    batchFull = true;
//                                }
//                            }
//                        }

                    if (!batchStrList.isEmpty()) {
                        ObjectReader reader = objectMapper.readerFor(SyncProtocolEvent.class);
                        List<Map<String, List<Protocol>>> protocolMapList = batchStrList.stream()
                                .map(json -> {
                                    try {
                                        SyncProtocolEvent syncProtocolEvent = reader.readValue(json);
                                        return syncProtocolEvent.getProtocolMap();
                                    } catch (JsonProcessingException e) {
                                        throw new RuntimeException("Error processing JSON", e);
                                    }
                                })
                                .collect(Collectors.toList());

                        // 合并数据
                        Map<String, List<Protocol>> mergedData = mergeData(protocolMapList);
                        RLock lock = redissonClient.getLock("processBatchLock");
                        if (lock.tryLock(5, TimeUnit.SECONDS)) {  // 尝试获取锁
                            try {
                                log.info("获取到分布式锁,开始处理数据");
                                processBatch(mergedData);
                            } finally {
                                lock.unlock();
                                log.info("释放分布式锁");
                            }
                        } else {
                            log.warn("未能获取分布式锁,跳过此次处理");
                        }

                    }
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                log.info("处理事件时线程被中断", e);


            } catch (Exception e) {
                log.error("处理事件时出现错误", e);
            }
        }
    });
}

/**
 * 批量合并事件数据,对于同一个id的协议结果,后插入的事件数据,覆盖前面的事件数据
 * @param batch
 * @return
 */
private static Map<String, List<Protocol>> mergeData(List<Map<String, List<Protocol>>> batch) {

    Map<String, List<Protocol>> mergedMap = new HashMap<>();

    for (Map<String, List<Protocol>> data : batch) {
        for (Map.Entry<String, List<Protocol>> entry : data.entrySet()) {
            String timeKey = entry.getKey();
            List<Protocol> protocols = entry.getValue();

            if (!mergedMap.containsKey(timeKey)) {
                mergedMap.put(timeKey, new ArrayList<>(protocols));
            } else {
                List<Protocol> existingProtocols = mergedMap.get(timeKey);
                Map<Long, Protocol> protocolMap = existingProtocols.stream()
                        .collect(Collectors.toMap(Protocol::getId, p -> p));

                for (Protocol protocol : protocols) {
                    protocolMap.put(protocol.getId(), protocol);
                }

                mergedMap.put(timeKey, new ArrayList<>(protocolMap.values()));
            }
        }
    }

    return mergedMap;
}

/**
 * 批量执行更新操作
 * @param mergedData
 */
private void processBatch(Map<String, List<Protocol>> mergedData) {

    // 缓存同步方法
    protocolService.incrementUpdate(mergedData);
}

缓存同步方法

    // 初始化本地缓存,设置过期时间和最大缓存数量
private Cache<String, List<Protocol>> localCache;

// 虚拟线程
ExecutorService virtualExecutor = Executors.newVirtualThreadPerTaskExecutor();

@PostConstruct
public void init() {

    this.localCache = Caffeine.newBuilder()
            .expireAfterWrite(10, TimeUnit.MINUTES)
            .maximumSize(1000)
            .build();

    // 初始化本地缓存
    loadCacheFromRedis();
}


/**
 * 同步协议变更到Redis缓存和本地缓存
 *
 * @param protocolMap 协议变更数据
 */
public void incrementUpdate(Map<String, List<Protocol>> protocolMap) {
    log.info("接收到同步协议结果到Redis的事件");
    long startTime = System.currentTimeMillis();

    if (protocolMap == null || protocolMap.isEmpty()) {
        log.info("同步的数据为空,直接返回");
        return;
    }

    // 并行处理新增、删除和修改数据
    protocolMap.entrySet().parallelStream().forEach(entry -> {
        String dateStr = entry.getKey();
        List<Protocol> protocols = entry.getValue();

        if (protocols == null || protocols.isEmpty()) {
            return;
        }

        // 创建锁对象
        RLock lock = redissonClient.getLock(String.format(RedisConstant.REDIS_PROTOCOL_SYNC_KEY, dateStr));
        try {
            // 尝试加锁,最多等待5秒,加锁成功后30秒自动解锁
            if (lock.tryLock(5, 30, TimeUnit.SECONDS)) {

                // 获取 Redis 中的 Map
                RMapCache<String, List<byte[]>> protocolMapRedis = redissonClient.getMapCache(RedisConstant.REDIS_PROTOCOL_INFO_KEY, StringJavaCodec.INSTANCE);

                // 设置过期时间
                protocolMapRedis.expire(1, TimeUnit.DAYS);

                // 获取当前日期对应的序列化数据
                List<byte[]> currentBytes = protocolMapRedis.get(dateStr);
                List<Protocol> currentProtocols = currentBytes == null ? new ArrayList<>() :
                        currentBytes.stream()
                                .map(bytes -> FuryUtils.deserialize(bytes, Protocol.class))
                                .collect(Collectors.toList());

                List<Protocol> toAdd = new ArrayList<>();
                List<Protocol> toRemove = new ArrayList<>();

                // 创建 Map,按照协议的 Id 进行分组,如果出现重复 Id,保留 updateTime 更大的协议(算是一个兜底的map处理方法,避免在不可预见的情况下产生了脏数据,返回正确数据,并清理脏数据)
                Map<Long, Protocol> protocolMapById = currentProtocols.stream()
                        .collect(Collectors.toMap(
                                Protocol::getId,
                                protocol -> protocol,
                                (existingProtocol, newProtocol) -> {

                                    // 如果 newProtocol 的 updateTime 更大,则保留新协议,并将旧协议加入 toRemove
                                    if (newProtocol.getUpdateTime().after(existingProtocol.getUpdateTime())) {

                                        // 将旧协议添加到 toRemove 列表
                                        toRemove.add(existingProtocol);
                                        return newProtocol;
                                    } else {

                                        // 将新协议添加到 toRemove 列表(因为旧协议更新)
                                        toRemove.add(newProtocol);
                                        return existingProtocol;
                                    }
                                }
                        ));

                // 处理协议变更,protocol.getUpdateStatus()的值会在协议计算的时候对应赋值
                protocols.forEach(protocol -> {
                    UpdateStatusEnum statusEnum = UpdateStatusEnum.getEnumByCode(protocol.getUpdateStatus());

                    if (statusEnum != null) {
                        switch (statusEnum) {
                            case ADD:
                                toAdd.add(protocol);
                                break;
                            case DELETE:
                                Protocol existing = protocolMapById.get(protocol.getId());
                                if (existing != null) {
                                    toRemove.add(existing);
                                }
                                break;
                            case UPDATE:
                                Protocol existingUpdate = protocolMapById.get(protocol.getId());
                                if (existingUpdate != null) {
                                    toRemove.add(existingUpdate);
                                }
                                toAdd.add(protocol);
                                break;
                            default:
                                break;
                        }
                    }
                });

                // 只有在有数据变更的情况下才进行更新操作
                boolean hasChanges = false;

                // 删除操作
                if (!toRemove.isEmpty()) {
                    currentProtocols.removeAll(toRemove);
                    hasChanges = true;
                }

                // 添加操作
                if (!toAdd.isEmpty()) {
                    currentProtocols.addAll(toAdd);
                    hasChanges = true;
                }

                // 只有在有变更的情况下才更新 Redis 和本地缓存
                if (hasChanges) {

                    log.info("协议变更: {},添加ids: {}, 删除ids: {}", dateStr,
                            toAdd.stream().map(Protocol::getId).collect(Collectors.toSet()),
                            toRemove.stream().map(Protocol::getId).collect(Collectors.toSet()));

                    List<byte[]> serializedUpdatedProtocols = currentProtocols.stream()
                            .map(FuryUtils::serialize)
                            .collect(Collectors.toList());

                    protocolMapRedis.put(dateStr, serializedUpdatedProtocols, 1, TimeUnit.DAYS);

                    // 更新本地缓存
                    localCache.put(dateStr, currentProtocols);
                }

            } else {
                log.warn("无法获取锁,跳过日期: {}", dateStr);
            }
        } catch (InterruptedException e) {

            log.error("同步协议变更时发生异常,清空本地缓存和Redis缓存", e);

            // 清空本地缓存
            localCache.invalidate(dateStr);

            // 清空 Redis 缓存
            RMapCache<String, List<byte[]>> protocolMapRedis = redissonClient.getMapCache(RedisConstant.REDIS_PROTOCOL_INFO_KEY, StringJavaCodec.INSTANCE);
            protocolMapRedis.clear();

            // 清空更新事件队列
            clearUpdateEventQueue();

            // 抛出运行时异常以中断流处理
            throw new RuntimeException("同步协议变更时发生致命异常,终止处理", e);

        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    });

    long endTime = System.currentTimeMillis();
    log.info("同步协议数据到Redis和本地缓存耗时: {} ms", endTime - startTime);
}

缓存查询

/**
 * 获取协议结果,先从本地缓存拿,再从Redis拿,最后从DB拿
 *
 * @param startDate 开始时间
 * @param endDate   结束时间
 * @return
 */
public List<Protocol> fetchProtocolForDateRange(Date startDate, Date endDate) {
    long s1 = DateUtil.currentSeconds();
    List<Protocol> result = new ArrayList<>();
    Set<String> missingDates = new HashSet<>();

    // 获取时间区间内所有日期
    List<String> dateRange = CommonUtils.getDateRange(startDate, endDate);
    List<Date> dates = new ArrayList<>();

    List<CompletableFuture<Void>> futures = new ArrayList<>();

    // 获取 Redis 中的 map 对象
    RMapCache<String, List<byte[]>> protocolMap = redissonClient.getMapCache(RedisConstant.REDIS_PROTOCOL_INFO_KEY, StringJavaCodec.INSTANCE);

    for (String date : dateRange) {
        CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(() -> {
            try {
                // 先从本地缓存获取
                List<Protocol> protocols = localCache.getIfPresent(date);
                if (protocols == null) {

                    // 本地缓存没有,再从Redis获取
                    List<byte[]> protocolBytes = protocolMap.get(date);
                    if (protocolBytes == null || protocolBytes.isEmpty()) {
                        synchronized (this) {
                            missingDates.add(date);
                        }
                    } else {
                        protocols = protocolBytes.stream()
                                .map(bytes -> FuryUtils.deserialize(bytes, Protocol.class))
                                .collect(Collectors.toList());
                        synchronized (this) {
                            result.addAll(protocols);

                            // 更新本地缓存
                            localCache.put(date, protocols);
                        }
                    }
                } else {
                    synchronized (this) {
                        result.addAll(protocols);
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, virtualExecutor);

        futures.add(completableFuture);
    }

    // 等待所有异步任务完成
    CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();

    long s2 = DateUtil.currentSeconds();
    log.info("协议查询redis耗时:{}", s2 - s1);

    // 如果有缺失的数据,从数据库查询并更新缓存
    if (!missingDates.isEmpty()) {
        log.info("查询数据库,更新缓存");
        List<Protocol> dbData = getSafeguardDataFromDB(missingDates);
        result.addAll(dbData);

        if (CollUtil.isNotEmpty(dbData)) {
            List<Date> dbDates = dbData.stream()
                    .map(Protocol::getSafeguardDate)
                    .collect(Collectors.toList());
            dates.addAll(dbDates);

            // 更新 Redis 和本地缓存
            for (String missingDate : missingDates) {
                List<Protocol> protocolsForDate = dbData.stream()
                        .filter(p -> missingDate.equals(DateUtil.formatDate(p.getSafeguardDate())))
                        .collect(Collectors.toList());
                if (!protocolsForDate.isEmpty()) {
                    List<byte[]> serializedProtocols = protocolsForDate.stream()
                            .map(FuryUtils::serialize)
                            .collect(Collectors.toList());

                    protocolMap.put(missingDate, serializedProtocols, 1, TimeUnit.DAYS);
                    localCache.put(missingDate, protocolsForDate);
                }
            }
        }
    }

    long s3 = DateUtil.currentSeconds();
    log.info("协议查询数据库耗时:{}", s3 - s2);
    log.info("协议查询总耗时:{}", s3 - s1);

    // 设置过期时间
    protocolMap.expire(1, TimeUnit.DAYS);
    return result;
}


/**
 * 从数据库查询并更新Redis缓存
 *
 * @param dates 日期列表
 * @return
 */
private List<Protocol> getSafeguardDataFromDB(Set<String> dates) {

    List<Protocol> dbProtocols = this.lambdaQuery()
            .in(Protocol::getSafeguardDate, dates)
            .list();

    if (CollUtil.isEmpty(dbProtocols)) {
        return new ArrayList<>();
    }

    return dbProtocols;
}

序列化,反序列化工具 Fury(很强大,感兴趣的可以去了解一下)

public class FuryUtils {

// 使用 ThreadSafeFury 保证多线程环境下安全
private static final ThreadSafeFury fury = Fury.builder()
        .withLanguage(Language.JAVA)
        .withCompatibleMode(CompatibleMode.COMPATIBLE) // 兼容模式
        .requireClassRegistration(false)  // 不强制类注册
        .buildThreadSafeFury();

/**
 * 序列化对象
 *
 * @param object 要序列化的对象
 * @param <T>    对象的类型
 * @return 序列化后的字节数组
 */
public static <T> byte[] serialize(T object) {
    if (object == null) {
        throw new IllegalArgumentException("The object to serialize cannot be null");
    }
    return fury.serialize(object);
}

/**
 * 反序列化对象
 *
 * @param bytes  要反序列化的字节数组
 * @param clazz  对象的 Class 类型
 * @param <T>    对象的类型
 * @return 反序列化后的对象
 */
public static <T> T deserialize(byte[] bytes, Class<T> clazz) {
    if (bytes == null || clazz == null) {
        throw new IllegalArgumentException("The byte array or class type cannot be null");
    }

    return (T) fury.deserialize(bytes);
}

/**
 * 深拷贝对象
 *
 * @param object 要拷贝的对象
 * @param <T>    对象的类型
 * @return 深拷贝后的对象
 */
public static <T> T deepCopy(T object) {
    if (object == null) {
        throw new IllegalArgumentException("The object to deep copy cannot be null");
    }
    byte[] serializedData = serialize(object);
    return deserialize(serializedData, (Class<T>) object.getClass());
}

脏数据发现与解决过程:

我的 redis缓存结构:Map<String,List> key是日期,value是该日期的协议数据

redis缓存更新的前提是,redis中有当前变更数据日期的key,才会更新。

航班日期会变更,如果航班A原来是2024-09-22 保障的,我同步到了key为2024-09-22的元素下,但是下一次增量航班数据查询的时候,航班A变成了2024-09-23保障的,跨天了这时候他会在2024-09-23插入一条数据,但是2024-09-22这个key还是会留下一条脏数据,没处理,这误导我以为缓存是被多线程还是异步影响了一致性。

解决方案:

    // 协议计算过程部分代码
    // 找出需要修改与更新的数据
    for (Protocol mergedProtocol : mergedList) {
        Protocol dbProtocol = dbProtocolMap.get(mergedProtocol.getUniqueKey());
        if (dbProtocol != null) {

            if (!dbProtocol.getMd5().equals(mergedProtocol.getMd5())) {
                mergedProtocol.setId(dbProtocol.getId());
               mergedProtocol.setUpdateStatus(UpdateStatusEnum.UPDATE.getCode());

                // 预防保障日期变更,产生缓存脏数据,重点在这里,保障日期出现变更的数据,我会oldSafeguardDate保留旧的保障日期
                if (!dbProtocol.getSafeguardDate().equals(mergedProtocol.getSafeguardDate())) {
                    mergedProtocol.setOldSafeguardDate(dbProtocol.getSafeguardDate());
                }
            }

        } else {
            mergedProtocol.setUpdateStatus(UpdateStatusEnum.ADD.getCode());
        }
    }

-----------------------------------------------------------------------------------------

    // allProtocols 是计算的结果
    final List<Protocol> finalProtocols = new ArrayList<>(allProtocols);
    if (CollUtil.isNotEmpty(allProtocols)) {

        // 清理保障日期变更的脏数据,会去脏数据原来的日期key中删除掉数据
        List<Protocol> dirtyData = updateData.stream()
                .filter(f -> Objects.nonNull(f.getOldSafeguardDate()))
                .map(p -> {
                    p.setSafeguardDate(p.getOldSafeguardDate())
                            .setUpdateStatus(UpdateStatusEnum.DELETE.getCode());
                    return p;
                })
                .collect(Collectors.toList());

        allProtocols.addAll(dirtyData);

        Map<String, List<Protocol>> dateListMap = allProtocols.stream()
                .collect(Collectors.groupingBy(protocol -> DateUtils.format(protocol.getSafeguardDate(), DateUtils.YYYY_MM_DD)));

        log.info("发送同步协议事件:{}", dateListMap.size());
        applicationContext.publishEvent(new SyncProtocolEvent(this, dateListMap));
    }

待实现功能:

问题: redis会一直累计fetch过的日期的航班,也许2天后就不会用到这些日期的航班了,但是数据还是一直保留在redis中

实现方案:保留当天fetch的最小日期,定时在0点删除redis中小于这个日期的数据。

总结:对于多生产者,多消费者的缓存同步,如何保障数据一致性这一功能,我还没实现过,后续如果走通了,会再补充进帖子中,关于缓存一致性的帖子,我在掘金看到了一篇很棒的文章,这里推荐一下    

作者:竹子爱熊猫

文章链接:(八)漫谈分布式之缓存篇:唠唠老生常谈的MySQL与Redis数据一致性问题!缓存既能减轻数据库压力,还能加快请求响应速 - 掘金

(如果推荐他人文章存在版权问题,可以私信我删除。有什么疑问或建议,欢迎在评论区讨论,如果想了解协议规则计算过程,也可以催更)