如何设计高并发点赞系统:基于RedisTemplate的架构实践
一、需求分析与技术选型
典型业务场景
- 每日千万级点赞请求
- 毫秒级响应延迟要求
- 防止用户重复点赞
- 实时展示点赞总数
- 支持查看点赞用户列表
技术选型理由
- Redis:单机10W+ QPS,天然适合计数器场景
- Hash结构:存储用户与内容的点赞关系
- String结构:存储点赞总数
- 异步队列:处理数据库持久化
- 分布式锁:解决并发竞争问题
二、系统架构设计
[客户端]
│
▼
[API网关] → [限流熔断]
│
▼
[点赞服务] ← Redis集群(存储实时数据)
│
▼
[RabbitMQ] → [持久化服务] → MySQL(最终一致性)
│
▼
[监控报警] ← Prometheus+Grafana
三、Redis数据结构设计
核心数据结构
- 点赞关系存储(Hash)
Key:like:{entityType}:{entityId}
Field:userId
Value:timestamp - 点赞计数器(String)
Key:like_count:{entityType}:{entityId}
Value:Integer - 用户行为记录(ZSet)
Key:user_like:{userId}
Score:timestamp
Value:{entityType}:{entityId}
四、Spring Boot核心实现代码
1. 依赖配置
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.23.2</version>
</dependency>
# application.properties
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.lettuce.pool.max-active=8
2. 核心服务实现
@Service
public class LikeService {
private final RedisTemplate<String, Object> redisTemplate;
private final RedissonClient redissonClient;
private final RabbitTemplate rabbitTemplate;
// 实体类型常量
private static final String ENTITY_TYPE_POST = "post";
private static final String ENTITY_TYPE_COMMENT = "comment";
@Autowired
public LikeService(RedisTemplate<String, Object> redisTemplate,
RedissonClient redissonClient,
RabbitTemplate rabbitTemplate) {
this.redisTemplate = redisTemplate;
this.redissonClient = redissonClient;
this.rabbitTemplate = rabbitTemplate;
}
/**
* 处理点赞/取消点赞
* @param userId 用户ID
* @param entityType 实体类型(帖子/评论)
* @param entityId 实体ID
* @return 最新的点赞状态和总数
*/
public Map<String, Object> likeOperation(Long userId, String entityType, Long entityId) {
// 使用Redisson分布式锁防止并发问题
RLock lock = redissonClient.getLock("like_lock:" + entityType + ":" + entityId);
try {
lock.lock(2, TimeUnit.SECONDS); // 最多等待2秒
String likeKey = "like:" + entityType + ":" + entityId;
String countKey = "like_count:" + entityType + ":" + entityId;
String userLikeKey = "user_like:" + userId;
// 检查是否已点赞
Boolean hasLiked = redisTemplate.opsForHash().hasKey(likeKey, userId.toString());
Map<String, Object> result = new HashMap<>();
if (Boolean.TRUE.equals(hasLiked)) {
// 取消点赞
redisTemplate.opsForHash().delete(likeKey, userId.toString());
redisTemplate.opsForValue().decrement(countKey);
redisTemplate.opsForZSet().remove(userLikeKey, entityType + ":" + entityId);
result.put("liked", false);
} else {
// 新增点赞
long timestamp = System.currentTimeMillis();
redisTemplate.opsForHash().put(likeKey, userId.toString(), timestamp);
redisTemplate.opsForValue().increment(countKey);
redisTemplate.opsForZSet().add(userLikeKey,
entityType + ":" + entityId,
timestamp);
result.put("liked", true);
// 发送MQ消息异步持久化
LikeMessage message = new LikeMessage(userId, entityType, entityId, timestamp);
rabbitTemplate.convertAndSend("like.exchange", "like.route", message);
}
// 获取最新点赞数
Object count = redisTemplate.opsForValue().get(countKey);
result.put("count", count != null ? count : 0);
return result;
} finally {
lock.unlock();
}
}
/**
* 获取实体点赞总数
*/
public Integer getLikeCount(String entityType, Long entityId) {
String countKey = "like_count:" + entityType + ":" + entityId;
Object count = redisTemplate.opsForValue().get(countKey);
return count != null ? Integer.parseInt(count.toString()) : 0;
}
/**
* 检查用户是否点赞
*/
public Boolean isUserLiked(Long userId, String entityType, Long entityId) {
String likeKey = "like:" + entityType + ":" + entityId;
return redisTemplate.opsForHash().hasKey(likeKey, userId.toString());
}
/**
* 获取用户最近点赞的N个实体
*/
public List<String> getRecentLikes(Long userId, int count) {
String userLikeKey = "user_like:" + userId;
Set<ZSetOperations.TypedTuple<Object>> tuples = redisTemplate.opsForZSet()
.reverseRangeWithScores(userLikeKey, 0, count - 1);
return tuples.stream()
.map(t -> t.getValue().toString())
.collect(Collectors.toList());
}
}
// 消息队列DTO
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LikeMessage implements Serializable {
private Long userId;
private String entityType;
private Long entityId;
private Long timestamp;
}
3. 持久化消费者
@Component
@RabbitListener(queues = "like.queue")
public class LikeConsumer {
@Autowired
private LikeRecordRepository likeRecordRepository;
@RabbitHandler
public void handleLikeMessage(LikeMessage message) {
// 将点赞记录写入MySQL
LikeRecord record = new LikeRecord();
record.setUserId(message.getUserId());
record.setEntityType(message.getEntityType());
record.setEntityId(message.getEntityId());
record.setCreatedAt(new Date(message.getTimestamp()));
likeRecordRepository.save(record);
// 更新MySQL中的点赞计数(使用CAS乐观锁)
likeRecordRepository.updateCount(
message.getEntityType(),
message.getEntityId(),
LocalDate.now()
);
}
}
五、核心设计思路解析
-
读写分离架构
- 实时操作:完全基于Redis内存操作
- 数据持久化:通过消息队列异步处理
- 最终一致性:允许短暂的数据延迟
-
防刷机制实现
// 在Service层添加限流注解 @Slf4j @Aspect @Component public class RateLimitAspect { private final RateLimiter rateLimiter = RateLimiter.create(100); // 每秒100次 @Around("@annotation(com.example.anno.RateLimit)") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { if (rateLimiter.tryAcquire()) { return joinPoint.proceed(); } else { log.warn("点赞频率过高被限制"); throw new RuntimeException("操作过于频繁"); } } } -
热点数据处理
- 本地缓存:对超级热帖的点赞数使用Caffeine缓存
java
复制
下载
@Cacheable(value = "hotLikeCount", key = "#entityType+':'+#entityId") public Integer getHotLikeCount(String entityType, Long entityId) { return getLikeCount(entityType, entityId); }
六、异常场景处理方案
-
缓存雪崩预防
// 设置随机过期时间 public void initLikeCount(String entityType, Long entityId, int count) { String key = "like_count:" + entityType + ":" + entityId; redisTemplate.opsForValue().set( key, count, 6 + (int)(Math.random() * 6), // 6-12小时随机过期 TimeUnit.HOURS ); } -
数据库降级策略
java
复制
下载
// 持久化失败时记录到死信队列 @RabbitListener(queues = "like.dlx.queue") public void handleFailedMessage(LikeMessage message) { log.error("持久化失败记录:{}", message); // 发送报警邮件/短信 } -
数据补偿机制
-- 每日凌晨执行数据校对 UPDATE entity_stats SET like_count = (SELECT COUNT(*) FROM like_records WHERE entity_type = 'post' AND entity_id = 123) WHERE entity_id = 123;
七、性能优化指标
| 指标项 | 目标值 | 监控方式 |
|---|---|---|
| Redis操作耗时 | <5ms | Prometheus |
| 持久化延迟 | <1分钟 | RabbitMQ监控 |
| 接口成功率 | >99.99% | Grafana |
| 并发处理能力 | >10W QPS | JMeter压测 |
八、面试高频问题解析
Q1:如何处理点赞数实时展示与数据库不一致?
A:采用分级缓存策略:
- 第一层:Redis实时计数
- 第二层:本地缓存(有效期10秒)
- 兜底方案:直接返回Redis数据,标记"实时"状态
Q2:怎么防止用户恶意刷赞?
A:三级防御体系:
- 前端:按钮防重复点击(禁用300ms)
- 网关:IP限流(滑动窗口算法)
- 服务端:用户维度计数(Redis incr+过期时间)
Q3:大V发帖瞬间流量暴涨怎么处理?
A:热点检测预案:
- 实时监控HSCAN发现热点Key
- 自动将热点数据分散到多个Redis节点
- 启用本地缓存+异步合并写入
Q4:如何实现点赞排行榜功能?
A:使用ZSet双重排序:
// 每天生成新的排行榜
String rankKey = "like_rank:" + LocalDate.now().toString();
redisTemplate.opsForZSet().add(rankKey, entityId, likeCount);
// 取TOP100
Set<Object> topEntities = redisTemplate.opsForZSet().reverseRange(rankKey, 0, 99);
九、总结与演进方向
系统亮点
- 双写一致性:通过消息队列保证最终一致性
- 高可用:Redis集群+故障自动转移
- 可扩展:无状态服务方便水平扩容
未来优化方向
- 引入布隆过滤器优化缓存穿透防护
- 使用Redis Stream实现更可靠的消息队列
- 接入ClickHouse进行点赞行为分析
- 探索RedisJSON处理复杂点赞关系
通过RedisTemplate构建的点赞系统,在保证高性能的同时,兼顾了系统的可靠性和可扩展性。这种设计模式可以扩展到其他计数器场景(收藏、浏览数等),是构建现代互联网应用的典型实践方案。