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的成员关系(使用Pipeline或mget)。
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学习。