🔥 高并发点赞系统实战:RedisTemplate 实现毫秒响应 + 防刷策略(附源码 & 大厂面试题拆解)

437 阅读5分钟

如何设计高并发点赞系统:基于RedisTemplate的架构实践

一、需求分析与技术选型

典型业务场景

  1. 每日千万级点赞请求
  2. 毫秒级响应延迟要求
  3. 防止用户重复点赞
  4. 实时展示点赞总数
  5. 支持查看点赞用户列表

技术选型理由

  • Redis:单机10W+ QPS,天然适合计数器场景
  • Hash结构:存储用户与内容的点赞关系
  • String结构:存储点赞总数
  • 异步队列:处理数据库持久化
  • 分布式锁:解决并发竞争问题

二、系统架构设计

[客户端]
   │
   ▼
[API网关][限流熔断]
   │
   ▼
[点赞服务] ← Redis集群(存储实时数据)
   │
   ▼
[RabbitMQ][持久化服务] → MySQL(最终一致性)
   │
   ▼
[监控报警] ← Prometheus+Grafana

三、Redis数据结构设计

核心数据结构

  1. 点赞关系存储(Hash)
    Key:like:{entityType}:{entityId}
    Field:userId
    Value:timestamp
  2. 点赞计数器(String)
    Key:like_count:{entityType}:{entityId}
    Value:Integer
  3. 用户行为记录(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()
        );
    }
}

五、核心设计思路解析

  1. 读写分离架构

    • 实时操作:完全基于Redis内存操作
    • 数据持久化:通过消息队列异步处理
    • 最终一致性:允许短暂的数据延迟
  2. 防刷机制实现

    // 在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("操作过于频繁");
            }
        }
    }
    
  3. 热点数据处理

    • 本地缓存:对超级热帖的点赞数使用Caffeine缓存

    java

    复制

    下载

    @Cacheable(value = "hotLikeCount", key = "#entityType+':'+#entityId")
    public Integer getHotLikeCount(String entityType, Long entityId) {
        return getLikeCount(entityType, entityId);
    }
    

六、异常场景处理方案

  1. 缓存雪崩预防

    // 设置随机过期时间
    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
        );
    }
    
  2. 数据库降级策略

    java

    复制

    下载

    // 持久化失败时记录到死信队列
    @RabbitListener(queues = "like.dlx.queue")
    public void handleFailedMessage(LikeMessage message) {
        log.error("持久化失败记录:{}", message);
        // 发送报警邮件/短信
    }
    
  3. 数据补偿机制

    -- 每日凌晨执行数据校对
    UPDATE entity_stats SET like_count = 
      (SELECT COUNT(*) FROM like_records 
       WHERE entity_type = 'post' AND entity_id = 123)
    WHERE entity_id = 123;
    

七、性能优化指标

指标项目标值监控方式
Redis操作耗时<5msPrometheus
持久化延迟<1分钟RabbitMQ监控
接口成功率>99.99%Grafana
并发处理能力>10W QPSJMeter压测

八、面试高频问题解析

Q1:如何处理点赞数实时展示与数据库不一致?
A:采用分级缓存策略:

  1. 第一层:Redis实时计数
  2. 第二层:本地缓存(有效期10秒)
  3. 兜底方案:直接返回Redis数据,标记"实时"状态

Q2:怎么防止用户恶意刷赞?
A:三级防御体系:

  1. 前端:按钮防重复点击(禁用300ms)
  2. 网关:IP限流(滑动窗口算法)
  3. 服务端:用户维度计数(Redis incr+过期时间)

Q3:大V发帖瞬间流量暴涨怎么处理?
A:热点检测预案:

  1. 实时监控HSCAN发现热点Key
  2. 自动将热点数据分散到多个Redis节点
  3. 启用本地缓存+异步合并写入

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集群+故障自动转移
  • 可扩展:无状态服务方便水平扩容

未来优化方向

  1. 引入布隆过滤器优化缓存穿透防护
  2. 使用Redis Stream实现更可靠的消息队列
  3. 接入ClickHouse进行点赞行为分析
  4. 探索RedisJSON处理复杂点赞关系

通过RedisTemplate构建的点赞系统,在保证高性能的同时,兼顾了系统的可靠性和可扩展性。这种设计模式可以扩展到其他计数器场景(收藏、浏览数等),是构建现代互联网应用的典型实践方案。