在内容平台系统中,收藏功能是最核心的用户交互场景之一。本文将详细介绍如何通过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天未访问数据归档到廉价存储
-
智能推荐:基于收藏行为的实时推荐算法
-
可视化监控:收藏数据实时大盘展示