高并发收藏功能设计:Redis异步同步与定时补偿机制详解

在内容平台系统中,收藏功能是最核心的用户交互场景之一。本文将详细介绍如何通过Redis、线程池和分布式锁实现高性能、高可用的收藏功能,确保百万级并发下的数据一致性。

一、整体架构设计

1.1 核心需求分析

  • 高频操作:支持每秒万级收藏操作

  • 实时响应:用户操作响应时间<100ms

  • 最终一致:Redis与MySQL数据最终一致

  • 容错机制:网络抖动或服务宕机时数据不丢失

1.2 技术架构设计

客户端 API网关 收藏操作 Redis读写 记录同步事件 线程池异步处理 MySQL同步 成功移除事件 定时补偿任务

二、核心代码实现与解析

2.1 收藏状态切换(doToggleCollect)

java 复制代码
private Map<String, Object> doToggleCollect(String userId, Integer articleId) {
    // 构造用户收藏Key:user:collect:1001
    String userCollectKey = String.format(USER_COLLECT_KEY, userId);
    
    // 检查收藏状态 - O(1)复杂度查询
    Boolean isCollected = redisTemplate.opsForHash().hasKey(userCollectKey, articleId.toString());
    
    int step;
    boolean newCollectStatus;
    
    if (Boolean.TRUE.equals(isCollected)) {
        // 取消收藏:删除Hash字段
        redisTemplate.opsForHash().delete(userCollectKey, articleId.toString());
        step = -1;
        newCollectStatus = false;
    } else {
        // 添加收藏:设置Hash字段
        redisTemplate.opsForHash().put(userCollectKey, articleId.toString(), "1");
        step = 1;
        newCollectStatus = true;
    }
    
    // 更新文章收藏计数 - 原子操作保证并发安全
    String countKey = String.format(ARTICLE_COLLECT_COUNT_KEY, articleId);
    redisTemplate.opsForValue().increment(countKey, step);
    
    // 创建同步事件对象
    CollectEvent event = new CollectEvent(userId, articleId, newCollectStatus);
    
    // 加入Redis同步队列(Set结构自动去重)
    redisTemplate.opsForSet().add(SYNC_QUEUE_KEY, event);
    
    // 异步执行数据库同步(线程池隔离)
    scheduler.execute(() -> syncToDatabase(event));
    
    // 清除用户推荐缓存 - 保证推荐结果实时性
    String recCacheKey = "user:rec:" + userId;
    redisTemplate.delete(recCacheKey);
    
    // 返回操作结果
    return Collections.singletonMap("isCollect", newCollectStatus);
}

关键技术点:

  • Hash结构存储:用户收藏状态使用Hash存储,key为user:collect:{userId},field为文章ID
  • 原子计数:文章收藏计数使用String结构,通过increment保证并发安全
  • 事件队列:使用Redis Set存储待同步事件,自动去重(依赖equals/hashCode)
  • 异步解耦:线程池处理耗时DB操作,保证主流程快速响应

2.2 数据库同步(syncToDatabase)

java 复制代码
@Transactional
public void syncToDatabase(CollectEvent event) {
    try {
        String userId = event.getUserId();
        Integer articleId = event.getArticleId();
        boolean isCollect = event.isCollect();
        
        // 查询现有记录 - 避免重复插入
        UserCollectRecord record = userBehaviorMapper.selectCollectOne(userId, articleId);
        
        if (record == null) {
            if (isCollect) {
                // 新增有效收藏记录
                UserCollectRecord newRecord = new UserCollectRecord();
                newRecord.setUserId(userId);
                newRecord.setArticleId(articleId);
                newRecord.setIsCancel(1); // 1=有效
                newRecord.setCreateTime(new Date());
                userBehaviorMapper.insertCollect(newRecord);
                
                // 更新文章收藏计数
                userArticlesMapper.updateCollectCount(articleId, 1);
            }
        } else {
            int newStatus = isCollect ? 1 : 2; // 1=有效,2=取消
            int step = isCollect ? 1 : -1;
            
            // 仅当状态变化时更新
            if (record.getIsCancel() != newStatus) {
                record.setIsCancel(newStatus);
                record.setUpdateTime(new Date());
                userBehaviorMapper.updateCollectById(record);
                
                // 更新计数
                userArticlesMapper.updateCollectCount(articleId, step);
            }
        }
        
        // 同步成功后移除事件
        redisTemplate.opsForSet().remove(SYNC_QUEUE_KEY, event);
    } catch (Exception e) {
        log.error("同步收藏数据到数据库失败: {}", event, e);
        retrySync(event); // 触发重试机制
    }
}

事务设计:

  • 使用@Transactional保证单次操作的原子性
  • 状态变化判断避免无效更新
  • 失败时进入重试流程

2.3 重试机制(retrySync)

java 复制代码
private void retrySync(CollectEvent event) {
    // 延迟10秒后重试 - 避开瞬时故障
    scheduler.schedule(() -> {
        // 检查事件是否仍在队列中(防止重复处理)
        if (redisTemplate.opsForSet().isMember(SYNC_QUEUE_KEY, event)) {
            log.info("重试同步收藏事件: {}", event);
            syncToDatabase(event);
        }
    }, 10, TimeUnit.SECONDS);
}

容错策略:

  • 延迟重试避免瞬时故障
  • Redis状态检查防止重复处理
  • 最多重试3次(代码未展示,实际需添加)

三、关键技术解析

3.1 Redis的核心作用

功能 数据结构 优势
用户收藏状态 Hash O(1)复杂度查询/更新
文章收藏计数 String 原子增减,避免并发冲突
同步事件队列 Set 自动去重,持久化存储
分布式锁 String 跨进程互斥控制

3.2 线程池(ScheduledExecutorService)

配置参数:

java 复制代码
// 创建定时线程池(5个核心线程)
private final ScheduledExecutorService scheduler = 
    Executors.newScheduledThreadPool(5);

核心作用:

  • 异步解耦:将DB操作与用户请求线程分离

  • 资源控制:限制最大并发DB连接数(防止连接池耗尽)

  • 任务调度:支持延迟任务(重试机制)

  • 队列缓冲:突发流量时积累任务,平滑处理

3.3 分布式锁优化实现

java 复制代码
// 加锁(含客户端标识防误删)
public boolean tryLock(String key, String clientId, long leaseTime) {
    return redisTemplate.execute((RedisCallback<Boolean>) connection -> {
        // 原子操作:SET key value NX PX timeout
        String result = connection.set(
            key.getBytes(),
            clientId.getBytes(),
            Expiration.milliseconds(leaseTime),
            RedisStringCommands.SetOption.SET_IF_ABSENT
        );
        return "OK".equals(result);
    });
}

// 解锁(Lua脚本保证原子性)
private static final String UNLOCK_SCRIPT = 
    "if redis.call('get', KEYS[1]) == ARGV[1] then " +
    "   return redis.call('del', KEYS[1]) " +
    "else " +
    "   return 0 " +
    "end";

public void releaseLock(String lockKey, String clientId) {
    redisTemplate.execute(
        new DefaultRedisScript<>(UNLOCK_SCRIPT, Long.class),
        Collections.singletonList(lockKey), 
        clientId
    );
}

使用场景:

  • 全量同步任务防多实例重复执行

  • 缓存初始化时避免多节点并发加载

  • 热点文章收藏的互斥控制

四、数据初始化与补偿机制

4.1 启动时数据加载

java 复制代码
@PostConstruct
public void initCollectCache() {
   // 加载用户收藏状态(仅有效状态)
   List<UserCollectRecord> collects = userBehaviorMapper.getAllCollectInformation();
   collects.stream()
       .filter(record -> record.getIsCancel() == 1)
       .forEach(record -> {
           String userKey = String.format(USER_COLLECT_KEY, record.getUserId());
           redisTemplate.opsForHash().put(userKey, record.getArticleId().toString(), "1");
       });
   
   // 加载文章收藏计数
   List<ArticleCollectCountDTO> counts = userArticlesMapper.getAllCollectCounts();
   counts.forEach(count -> {
       String countKey = String.format(ARTICLE_COLLECT_COUNT_KEY, count.getArticleId());
       redisTemplate.opsForValue().set(countKey, count.getCollectCount());
   });
}

4.2 定时全量同步(每5分钟)

java 复制代码
@Scheduled(fixedRate = 5 * 60 * 1000)
public void fullSyncTask() {
    // 处理积压事件
    Set<Object> events = redisTemplate.opsForSet().members(SYNC_QUEUE_KEY);
    events.forEach(event -> {
        if (event instanceof CollectEvent) {
            syncToDatabase((CollectEvent) event);
        }
    });
    
    // 校验计数一致性
    Set<String> countKeys = redisTemplate.keys(ARTICLE_COLLECT_COUNT_KEY.replace("%s", "*"));
    countKeys.forEach(key -> {
        String[] parts = key.split(":");
        Integer articleId = Integer.parseInt(parts[parts.length-1]);
        Long redisCount = Long.valueOf(redisTemplate.opsForValue().get(key).toString());
        Long dbCount = userArticlesMapper.getCollectCount(articleId);
        
        if (!redisCount.equals(dbCount)) {
            // 以Redis为准修复
            int diff = (int)(redisCount - dbCount);
            userArticlesMapper.updateCollectCount(articleId, diff);
            log.warn("修复计数不一致: 文章{} Redis={} DB={}", articleId, redisCount, dbCount);
        }
    });
}

双重保障机制:

  • 实时同步:用户操作后立即触发异步同步

  • 定时补偿:5分钟全量校对修复不一致

  • 重试机制:单条记录失败后延迟重试

五、性能优化方案

5.1 事件队列升级(Redis Streams)

java 复制代码
// 生产事件
Map<String, String> fields = new HashMap<>();
fields.put("userId", event.getUserId());
fields.put("articleId", event.getArticleId().toString());
fields.put("status", String.valueOf(event.isCollect()));
redisTemplate.opsForStream().add("collect_events", fields);

// 消费事件(消费者组)
while (true) {
    List<MapRecord<String, String, String>> records = redisTemplate.opsForStream()
        .read(Consumer.from("group1", "consumer1"),
            StreamReadOptions.empty().count(100),
            StreamOffset.create("collect_events", ReadOffset.lastConsumed()));
    
    if (!records.isEmpty()) {
        processEvents(records); // 批量处理
        redisTemplate.opsForStream().acknowledge("group1", records); // ACK确认
    }
}

优势:

  • 消息顺序保证

  • 消费者组负载均衡

  • 消息确认机制

  • 断点续传能力

5.2 热点文章处理

java 复制代码
// 存储时(10个分片)
int shard = articleId % 10;
String shardKey = "article:collect:shard:" + shard;
redisTemplate.opsForHash().increment(shardKey, articleId.toString(), step);

// 查询时
public long getCollectCount(Integer articleId) {
    int shard = articleId % 10;
    String shardKey = "article:collect:shard:" + shard;
    Object count = redisTemplate.opsForHash().get(shardKey, articleId.toString());
    return count != null ? Long.parseLong(count.toString()) : 0;
}

优势:

  • 将热点Key分散到多个Hash

  • 避免单个Key的访问过热

  • 保持原子操作特性

5.3 缓存优化策略

命中 未命中 命中 未命中 用户请求 本地缓存 直接返回 查询Redis 返回并写本地缓存 查询DB 写Redis 返回

多级缓存方案:

  • 本地缓存:Caffeine(存储用户收藏状态)

  • Redis缓存:存储全量数据

  • DB:持久化存储

优势:

  • 减少Redis访问量(尤其高频访问用户)

  • 平均响应时间降低30-50%

  • 成本显著下降

六、总结与展望

6.1 方案优势

  • 高性能:Redis操作<5ms,满足10万+QPS

  • 高可用:故障时通过补偿机制保证数据不丢失

  • 弹性扩展:无状态设计支持水平扩容

  • 成本可控:Redis内存占用优化,DB压力降低90%

6.2 未来优化方向

  • 跨机房同步:通过Redis Cluster实现多机房数据同步

  • 冷热分离:将30天未访问数据归档到廉价存储

  • 智能推荐:基于收藏行为的实时推荐算法

  • 可视化监控:收藏数据实时大盘展示

相关推荐
火龙谷20 分钟前
【nosql】有哪些非关系型数据库?
数据库·nosql
勤奋的小王同学~21 分钟前
(javaEE初阶)计算机是如何组成的:CPU基本工作流程 CPU介绍 CPU执行指令的流程 寄存器 程序 进程 进程控制块 线程 线程的执行
java·java-ee
TT哇23 分钟前
JavaEE==网站开发
java·redis·java-ee
2401_8260976227 分钟前
JavaEE-Linux环境部署
java·linux·java-ee
缘来是庄1 小时前
设计模式之访问者模式
java·设计模式·访问者模式
天水幼麟1 小时前
动手学深度学习-学习笔记【二】(基础知识)
笔记·深度学习·学习
焱焱枫1 小时前
Oracle获取执行计划之10046 技术详解
数据库·oracle
Bug退退退1231 小时前
RabbitMQ 高级特性之死信队列
java·分布式·spring·rabbitmq
沧海一笑-dj2 小时前
【51单片机】51单片机学习笔记-课程简介
笔记·学习·51单片机·江科大·江科大学习笔记·江科大单片机·江科大51单片机
梵高的代码色盘2 小时前
后端树形结构
java