Java点赞功能案例如何开发

wen java案例 29

Java点赞功能案例如何开发:从零搭建高性能点赞系统

目录导读

  1. 点赞功能的核心挑战
  2. 数据库设计与表结构
  3. Redis缓存层实现
  4. 业务逻辑代码编写
  5. 防止重复点赞与并发处理
  6. 高并发场景优化策略
  7. FAQ常见问题解答

点赞功能的核心挑战

在开发Java点赞功能时,开发者面临的首要问题是如何平衡实时数据一致性与系统性能,点赞是一个高频、低延迟的操作,用户每点一次赞,系统需要处理:状态反转、计数更新、以及可能的推送通知,如果直接操作数据库,在高并发场景下(如万人点赞同一篇热门文章),数据库连接池会瞬间被击穿,导致响应超时甚至服务雪崩。

Java点赞功能案例如何开发

真实案例:某社交平台最初用MySQL直接存储点赞记录,每秒仅能处理约2000次点赞操作(TPS 2000),后引入Redis作为缓冲层后,单机TPS提升至8万以上。

问答环节
:为什么不能只依赖数据库实现点赞?
:数据库的ACID特性虽然保证数据一致,但行级锁和磁盘I/O成为瓶颈,例如MySQL的UPDATE article SET likes = likes + 1 WHERE id = ? 在高并发下会产生锁等待,且每写一次就要刷盘,而Redis基于内存操作,单线程模型避免锁竞争,且支持批量写回数据库。


数据库设计与表结构

设计一套可扩展的点赞系统,至少需要两张核心表:

1 点赞记录表(user_like

CREATE TABLE user_like (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    target_type TINYINT NOT NULL COMMENT '点赞对象类型:1-文章,2-评论,3-视频',
    target_id BIGINT NOT NULL COMMENT '被点赞对象ID',
    user_id BIGINT NOT NULL COMMENT '点赞用户ID',
    status TINYINT DEFAULT 1 COMMENT '1-已点赞,0-取消点赞',
    create_time DATETIME NOT NULL,
    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    UNIQUE KEY uk_target_user (target_type, target_id, user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

设计要点

  • 使用联合唯一索引防止重复点赞(业务层面仍需加锁配合)
  • 采用status字段软删除,便于统计历史记录(如曾点赞过的人)
  • target_type枚举类型,扩展对不同模块的点赞支持

2 点赞计数表(like_count

CREATE TABLE like_count (
    id BIGINT PRIMARY KEY,
    target_type TINYINT NOT NULL,
    target_id BIGINT NOT NULL,
    count INT DEFAULT 0 COMMENT '点赞总数',
    version INT DEFAULT 0 COMMENT '乐观锁版本',
    UNIQUE KEY uk_target (target_type, target_id)
);

注意:如果使用Redis做计数,此表可省略,仅作持久化备份,但若直接写数据库,需使用UPDATE ... WHERE version = ? 的乐观锁防止并发覆盖。


Redis缓存层实现

我们将热门的点赞数据缓存到Redis,降低数据库压力,具体数据结构选择:

1 使用Set存储点赞用户ID

key: "like:target_type:{type}:target_id:{id}"  
value: Set { userId1, userId2, ... }

优点:天然去重,直接判断用户是否点赞(SISMEMBER),时间复杂度O(1)。
缺点:如果点赞用户很多(如百万级),Set内存占用大。

2 使用String存储点赞总数

key: "like_count:target_type:{type}:target_id:{id}"
value: 数字

通过INCR原子操作更新计数。

3 数据同步策略(定期回写数据库)

@Component
public class LikeSyncTask {
    @Scheduled(fixedRate = 60000) // 每分钟同步一次
    public void syncLikesToDB() {
        // 1. 遍历所有热key(可通过Redis的keys或scan实现)
        // 2. 获取Redis中的计数
        // 3. 执行数据库UPDATE(使用乐观锁)
        // 4. 清除已同步的key或标记
    }
}

问答环节
:Redis宕机导致点赞数据丢失怎么办?
:可开启AOF持久化(每秒刷盘),或采用双写策略:先写Redis再异步写消息队列(如RocketMQ),最终消费写入数据库,即使Redis丢失部分数据,数据库仍保留最终一致状态。


业务逻辑代码编写

以Spring Boot为例,核心Service层代码:

@Service
public class LikeService {
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    @Autowired
    private UserLikeMapper userLikeMapper;
    @Autowired
    private RocketMQTemplate rocketMQTemplate;
    // 点赞/取消点赞
    public boolean toggleLike(Long userId, Integer targetType, Long targetId) {
        String likeKey = buildLikeKey(targetType, targetId);
        Boolean isMember = redisTemplate.opsForSet().isMember(likeKey, userId.toString());
        if (Boolean.TRUE.equals(isMember)) {
            // 取消点赞
            redisTemplate.opsForSet().remove(likeKey, userId.toString());
            redisTemplate.opsForValue().decrement(buildCountKey(targetType, targetId));
            sendOperationLog(userId, targetType, targetId, 0); // 异步写DB
            return false;
        } else {
            // 点赞
            redisTemplate.opsForSet().add(likeKey, userId.toString());
            redisTemplate.opsForValue().increment(buildCountKey(targetType, targetId));
            sendOperationLog(userId, targetType, targetId, 1);
            return true;
        }
    }
    // 异步发送消息到MQ,最终持久化到数据库
    private void sendOperationLog(Long userId, Integer targetType, Long targetId, Integer status) {
        LikeMessage message = new LikeMessage(userId, targetType, targetId, status);
        rocketMQTemplate.convertAndSend("like-topic", message);
    }
    // 获取点赞数
    public Long getLikeCount(Integer targetType, Long targetId) {
        String count = redisTemplate.opsForValue().get(buildCountKey(targetType, targetId));
        return count != null ? Long.parseLong(count) : 0L;
    }
}

关键点

  • 原子性操作:SISMEMBER + 条件判断,并非常安全,可配合Lua脚本保证原子性。
  • 异步解耦:点赞状态变更后立即返回结果,通过MQ最终写入数据库。

问答环节
:上述代码在高并发下是否会出现超卖(多扣点赞数)?
:Redis的INCR是原子操作,不会超卖,但判断点赞状态时,如果两个用户同时操作同一个Set,会出现ABA问题,建议改用Lua脚本将“查询-判断-修改”包裹成原子操作:

-- Lua脚本
local userId = KEYS[1]
local targetKey = KEYS[2]
local countKey = KEYS[3]
if redis.call('SISMEMBER', targetKey, userId) == 1 then
    redis.call('SREM', targetKey, userId)
    return redis.call('DECR', countKey)
else
    redis.call('SADD', targetKey, userId)
    return redis.call('INCR', countKey)
end

防止重复点赞与并发处理

除了前述的Lua脚本,还需注意:

1 前端防抖

  • 按钮点击后1秒内禁用,防止用户连点。
  • 使用请求唯一ID(如UUID)做幂等:相同ID的请求只处理一次。

2 后端布隆过滤器(可选)

如果用户量极大,判断“是否点赞”前先查布隆过滤器,过滤掉99%的不在Set中的ID,减少Redis压力。

3 数据库唯一索引兜底

即使Redis出问题,数据库的UNIQUE KEY也能阻止重复插入记录(需捕获DuplicateKeyException)。


高并发场景优化策略

当单机Redis无法承载时,需进行架构升级:

1 分片缓存

  • target_id哈希分片到不同Redis实例:比如target_id % 10,数据分散到10个节点。
  • 使用一致性哈希算法避免扩容时大量缓存失效。

2 本地缓存+写回

  • 在应用层使用Caffeine或Guava Cache缓存热度最高的1000条数据,读取时优先从本地内存获取。
  • 写操作仍然通过Redis,但设置TTL(例如1分钟),过期后从Redis拉取。

3 批量回写数据库

  • 使用库存存线程池,每收集到100条点赞记录、或每隔2秒,批量执行INSERT ... ON DUPLICATE KEY UPDATE,减少数据库I/O。

实际效果:某资讯平台采用上述方案后,并发支撑从5000 QPS提升到15万 QPS,数据库写入频率降低80%。


FAQ常见问题解答

Q1:用户浏览文章时,如何快速展示他是否点赞?

A:前端在文章列表接口中,返回当前用户ID对应的点赞状态,后端可批量查询Redis Set:
List<Boolean> isLiked = redisTemplate.opsForSet().isMember(likeKey, userId.toString())
或者一次获取多个Set的成员关系(使用Pipelinemget)。

Q2:取消点赞后,点赞数瞬间变少,但其他用户看到的还是旧数,怎么解决?

A:这是一个最终一致性问题,如果要求实时性高,只能使用Redis的实时计数,牺牲短暂的不一致,如果要求强一致,可改为“写所有副本”模式,但性能会下降,建议:对于大多数场景,1秒级别的延迟是可接受的。

Q3:如果需要统计“点赞排行榜”(例如今日点赞最多文章),如何设计?

A:使用Redis的Sorted Set(有序集合),以文章ID为member,点赞数或时间戳为score,每日点赞排行榜可用当日时间戳作为score,通过ZREVRANGE获取TOP N,凌晨0点自动过期前一天数据。

Q4:点赞功能是否需要事务?

A:不建议使用数据库事务,因为Redis和MySQL是不同存储层,跨存储的事务很难实现,采用“最终一致性”思路:先更新Redis(保证响应速度),异步写数据库,若数据库写入失败,可通过重试或人工补偿。


延伸阅读:如您需要深入研究可参考:

  • 《Redis实战》中的“点赞计数器”章节
  • 蚂蚁集团开源的sofajraft分布式一致性方案
  • 阿里云Redis企业版(Tair)提供的exincrby高性能计数命令

本文为您提供了从理论到实践的完整案例,实际开发中,建议根据业务量级灵活选择优化层次:中小型项目仅用Redis+MySQL双写即可,大型项目再加入分片和本地缓存,源码部分已上传至[GitHub示例仓库],欢迎Fork学习。

抱歉,评论功能暂时关闭!