news 2026/5/8 16:21:40

从大众点评到Redis实战:手把手教你用SpringBoot+Redis实现签到、点赞、关注功能

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从大众点评到Redis实战:手把手教你用SpringBoot+Redis实现签到、点赞、关注功能

从大众点评到Redis实战:手把手教你用SpringBoot+Redis实现签到、点赞、关注功能

在当今互联网应用中,高并发场景下的数据处理一直是开发者面临的挑战。以社交电商平台为例,用户签到、内容点赞和关注关系这些高频操作,如果直接依赖传统数据库,很容易导致系统性能瓶颈。Redis作为内存数据库,凭借其出色的读写性能和丰富的数据结构,成为解决这类问题的利器。

本文将带您深入实战,基于SpringBoot框架,利用Redis的BitMap、ZSet和Set等数据结构,完整实现三个典型业务场景。不同于简单的API调用演示,我们会从数据结构选型、并发控制到代码优化,逐步拆解每个环节的设计思考。

1. 环境准备与基础配置

1.1 项目初始化

首先创建一个标准的SpringBoot项目,引入必要的依赖:

<dependencies> <!-- SpringBoot基础依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Redis集成 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- Lombok简化代码 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>

在application.yml中配置Redis连接:

spring: redis: host: 127.0.0.1 port: 6379 password: database: 0

1.2 RedisTemplate配置

为了更高效地操作Redis,我们需要自定义RedisTemplate配置:

@Configuration public class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); // 使用Jackson序列化 Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class); ObjectMapper mapper = new ObjectMapper(); mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); mapper.activateDefaultTyping(mapper.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL); serializer.setObjectMapper(mapper); // 设置序列化方式 template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(serializer); template.setHashKeySerializer(new StringRedisSerializer()); template.setHashValueSerializer(serializer); template.afterPropertiesSet(); return template; } }

2. 签到功能实现:BitMap的妙用

2.1 BitMap原理解析

签到功能的特点是:

  • 数据量大(每个用户每月一条记录)
  • 只需记录是否签到(二值状态)
  • 需要快速统计连续签到天数

传统关系型数据库实现会面临:

  • 单表数据量膨胀快
  • 统计查询效率低下
  • 存储空间浪费

Redis的BitMap完美匹配这些需求:

  • 每个用户每月仅需约4字节存储
  • 支持位运算快速统计
  • 时间复杂度O(1)的读写操作

2.2 核心代码实现

创建签到服务接口:

public interface SignService { /** * 用户签到 * @param userId 用户ID * @return 签到结果 */ boolean sign(Long userId); /** * 获取连续签到天数 * @param userId 用户ID * @return 连续签到天数 */ int getContinuousSignCount(Long userId); /** * 获取当月签到情况 * @param userId 用户ID * @return 签到bitmap */ Map<String, Boolean> getSignInfo(Long userId); }

具体实现类:

@Service public class SignServiceImpl implements SignService { private final RedisTemplate<String, Object> redisTemplate; private static final String SIGN_KEY_PREFIX = "user:sign:"; @Override public boolean sign(Long userId) { LocalDate now = LocalDate.now(); String key = buildSignKey(userId, now); int dayOfMonth = now.getDayOfMonth(); return redisTemplate.opsForValue().setBit(key, dayOfMonth - 1, true); } @Override public int getContinuousSignCount(Long userId) { LocalDate now = LocalDate.now(); String key = buildSignKey(userId, now); int dayOfMonth = now.getDayOfMonth(); // 获取当月签到bitmap List<Long> bitField = redisTemplate.execute( (RedisCallback<List<Long>>) conn -> conn.bitField(key.getBytes(), BitFieldSubCommands.create() .get(BitFieldType.unsigned(dayOfMonth)) .valueAt(0))); if (bitField == null || bitField.isEmpty()) { return 0; } long signFlag = bitField.get(0) == null ? 0 : bitField.get(0); int count = 0; // 通过位运算计算连续签到天数 while ((signFlag & 1) != 0) { count++; signFlag >>>= 1; } return count; } private String buildSignKey(Long userId, LocalDate date) { return SIGN_KEY_PREFIX + userId + ":" + date.format(DateTimeFormatter.ofPattern("yyyyMM")); } }

2.3 性能优化技巧

  1. 键设计优化

    • 使用user:sign:{userId}:{yyyyMM}格式
    • 避免大key问题(单用户单月数据)
  2. 内存优化

    • Redis的BitMap会自动扩容,但建议预判最大位数
    • 对于历史数据,可以定期归档到数据库
  3. 统计优化

    • 使用BITFIELD命令一次获取多个月数据
    • 客户端缓存热门用户的签到状态

3. 点赞功能实现:ZSet的精准控制

3.1 数据结构选型分析

点赞功能需要考虑:

  • 记录谁点了赞(用户ID)
  • 点赞时间排序(展示最新点赞)
  • 防止重复点赞
  • 点赞计数

对比几种数据结构:

数据结构优点缺点适用场景
String简单无法记录用户信息不适用
Set去重无序简单点赞
ZSet有序+去重稍复杂需要排序的点赞

显然ZSet(有序集合)是最佳选择:

  • 成员=用户ID(自动去重)
  • 分数=时间戳(自然排序)

3.2 核心业务实现

点赞服务接口设计:

public interface LikeService { /** * 点赞或取消点赞 * @param userId 用户ID * @param targetType 目标类型(1:博客 2:评论) * @param targetId 目标ID * @return 当前点赞状态 */ boolean toggleLike(Long userId, Integer targetType, Long targetId); /** * 获取点赞列表 * @param targetType 目标类型 * @param targetId 目标ID * @param page 页码 * @param size 每页数量 * @return 点赞用户ID列表 */ List<Long> getLikedUsers(Integer targetType, Long targetId, int page, int size); /** * 获取点赞数量 * @param targetType 目标类型 * @param targetId 目标ID * @return 点赞数 */ Long getLikeCount(Integer targetType, Long targetId); }

具体实现:

@Service public class LikeServiceImpl implements LikeService { private final RedisTemplate<String, Object> redisTemplate; private static final String LIKE_KEY_PREFIX = "like:"; private static final String USER_LIKE_KEY_PREFIX = "user_like:"; @Override public boolean toggleLike(Long userId, Integer targetType, Long targetId) { String key = buildKey(targetType, targetId); String userLikeKey = USER_LIKE_KEY_PREFIX + userId; // 判断是否已点赞 Double score = redisTemplate.opsForZSet().score(key, userId.toString()); boolean isLike; if (score == null) { // 点赞操作 redisTemplate.opsForZSet().add(key, userId.toString(), System.currentTimeMillis()); redisTemplate.opsForSet().add(userLikeKey, targetType + ":" + targetId); isLike = true; } else { // 取消点赞 redisTemplate.opsForZSet().remove(key, userId.toString()); redisTemplate.opsForSet().remove(userLikeKey, targetType + ":" + targetId); isLike = false; } return isLike; } @Override public List<Long> getLikedUsers(Integer targetType, Long targetId, int page, int size) { String key = buildKey(targetType, targetId); // 分页获取点赞用户ID(按时间倒序) Set<Object> userIds = redisTemplate.opsForZSet() .reverseRange(key, page * size, (page + 1) * size - 1); return userIds.stream() .map(id -> Long.parseLong(id.toString())) .collect(Collectors.toList()); } private String buildKey(Integer targetType, Long targetId) { return LIKE_KEY_PREFIX + targetType + ":" + targetId; } }

3.3 高级特性实现

点赞计数优化

// 使用ZCard获取有序集合大小,比维护单独计数器更可靠 @Override public Long getLikeCount(Integer targetType, Long targetId) { return redisTemplate.opsForZSet() .size(buildKey(targetType, targetId)); }

用户点赞历史查询

public List<String> getUserLikeHistory(Long userId) { String key = USER_LIKE_KEY_PREFIX + userId; Set<Object> likedItems = redisTemplate.opsForSet().members(key); return likedItems.stream() .map(Object::toString) .collect(Collectors.toList()); }

4. 关注功能实现:Set的交并集运算

4.1 关系型数据建模

关注关系的特点是:

  • 需要记录双向关系(关注者与被关注者)
  • 需要快速查询共同关注
  • 需要统计粉丝/关注数量

传统数据库需要多表关联查询,而Redis的Set结构可以轻松实现:

  • followers:{userId}:存储粉丝集合
  • following:{userId}:存储关注集合

4.2 核心业务代码

关注服务接口:

public interface FollowService { /** * 关注/取消关注 * @param followerId 关注者ID * @param followeeId 被关注者ID * @param isFollow 是否关注 * @return 操作结果 */ boolean follow(Long followerId, Long followeeId, boolean isFollow); /** * 获取共同关注列表 * @param userId1 用户1 ID * @param userId2 用户2 ID * @return 共同关注用户ID列表 */ List<Long> getCommonFollows(Long userId1, Long userId2); /** * 分页获取粉丝列表 * @param userId 用户ID * @param page 页码 * @param size 每页大小 * @return 粉丝ID列表 */ List<Long> getFollowers(Long userId, int page, int size); /** * 分页获取关注列表 * @param userId 用户ID * @param page 页码 * @param size 每页大小 * @return 关注用户ID列表 */ List<Long> getFollowings(Long userId, int page, int size); }

具体实现:

@Service public class FollowServiceImpl implements FollowService { private final RedisTemplate<String, Object> redisTemplate; private static final String FOLLOWERS_KEY_PREFIX = "followers:"; private static final String FOLLOWING_KEY_PREFIX = "following:"; @Override public boolean follow(Long followerId, Long followeeId, boolean isFollow) { String followersKey = FOLLOWERS_KEY_PREFIX + followeeId; String followingKey = FOLLOWING_KEY_PREFIX + followerId; if (isFollow) { // 关注操作 redisTemplate.opsForSet().add(followersKey, followerId.toString()); redisTemplate.opsForSet().add(followingKey, followeeId.toString()); } else { // 取消关注 redisTemplate.opsForSet().remove(followersKey, followerId.toString()); redisTemplate.opsForSet().remove(followingKey, followeeId.toString()); } return isFollow; } @Override public List<Long> getCommonFollows(Long userId1, Long userId2) { String followingKey1 = FOLLOWING_KEY_PREFIX + userId1; String followingKey2 = FOLLOWING_KEY_PREFIX + userId2; // 使用SINTER命令获取集合交集 Set<Object> common = redisTemplate.opsForSet() .intersect(followingKey1, followingKey2); return common.stream() .map(id -> Long.parseLong(id.toString())) .collect(Collectors.toList()); } }

4.3 性能优化方案

  1. 大集合处理

    • 对于粉丝量大的用户,使用SCAN代替SMEMBERS
    • 考虑分片存储(如followers:{userId}:shard1
  2. 计数优化

    // 使用SCARD命令获取集合大小 public Long getFollowerCount(Long userId) { return redisTemplate.opsForSet() .size(FOLLOWERS_KEY_PREFIX + userId); }
  3. 关系同步

    • 异步将重要关系同步到数据库
    • 定时任务修复不一致数据

5. 生产环境进阶实践

5.1 缓存一致性保障

Redis与数据库的双写问题解决方案:

  1. Cache Aside Pattern

    • 读:先查缓存,未命中查DB并回填
    • 写:先更新DB,再删除缓存
  2. 事务消息

    • 通过消息队列保证最终一致性
    • 实现补偿机制

示例代码:

@Transactional public void updateUser(User user) { // 1. 更新数据库 userMapper.updateById(user); // 2. 删除缓存 redisTemplate.delete("user:" + user.getId()); // 3. 发送消息(可选) rocketMQTemplate.send("user-update-topic", MessageBuilder.withPayload(user.getId()).build()); }

5.2 高并发优化

点赞接口的并发控制

public boolean toggleLikeWithLock(Long userId, Integer targetType, Long targetId) { String lockKey = "like:lock:" + targetType + ":" + targetId + ":" + userId; String lockValue = UUID.randomUUID().toString(); try { // 尝试获取分布式锁 Boolean locked = redisTemplate.opsForValue() .setIfAbsent(lockKey, lockValue, 30, TimeUnit.SECONDS); if (Boolean.TRUE.equals(locked)) { return toggleLike(userId, targetType, targetId); } throw new RuntimeException("操作太频繁"); } finally { // 释放锁 if (lockValue.equals(redisTemplate.opsForValue().get(lockKey))) { redisTemplate.delete(lockKey); } } }

5.3 监控与报警

关键监控指标:

  1. Redis健康状态

    • 内存使用率
    • 连接数
    • 命中率
  2. 业务指标

    # 监控签到用户数 redis-cli --stat | grep 'user:sign' # 监控点赞QPS redis-cli info stats | grep total_commands_processed

推荐监控工具:

  • Prometheus + Grafana
  • Redis自带的INFO命令
  • 阿里云/腾讯云等云监控服务

6. 踩坑经验分享

在实际项目中,我们遇到过几个典型问题:

  1. BitMap的月份切换

    • 每月1日需要自动创建新key
    • 解决方案:使用exists命令检查key是否存在
  2. ZSet的内存增长

    • 长期运行后热门内容点赞集合过大
    • 解决方案:定期归档历史数据到DB
  3. Set的交集性能

    • 大集合求交集可能导致阻塞
    • 解决方案:在从节点执行或使用SCAN分批处理

一个实用的调试技巧是使用Redis的slowlog命令找出性能瓶颈:

# 查看慢查询 redis-cli slowlog get 10

7. 扩展思考

7.1 数据持久化策略

根据业务重要性选择不同策略:

数据类型持久化策略同步频率备注
签到数据RDB+AOF每天全量可接受少量丢失
点赞关系AOF实时重要关系数据
关注关系RDB+AOF每小时可重建

7.2 多级缓存设计

对于超高并发场景,可以考虑:

  1. 本地缓存(Caffeine)→ Redis → DB
  2. 热点数据预加载
  3. 缓存空对象防止穿透

示例配置:

@Configuration public class CacheConfig { @Bean public CacheManager cacheManager() { CaffeineCacheManager cacheManager = new CaffeineCacheManager(); cacheManager.setCaffeine(Caffeine.newBuilder() .expireAfterWrite(10, TimeUnit.MINUTES) .maximumSize(1000)); return cacheManager; } }

7.3 未来演进方向

  1. Redis集群化:应对数据增长
  2. 读写分离:提升查询性能
  3. 冷热分离:热数据存Redis,冷数据存DB
  4. AI预测:基于用户行为预加载数据
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/8 16:21:06

5分钟掌握GPX Studio:免费在线GPX编辑器完全指南

5分钟掌握GPX Studio&#xff1a;免费在线GPX编辑器完全指南 【免费下载链接】gpxstudio.github.io The online GPX file editor 项目地址: https://gitcode.com/gh_mirrors/gp/gpxstudio.github.io 想要轻松编辑GPS轨迹文件却苦于没有专业软件&#xff1f;GPX Studio作…

作者头像 李华
网站建设 2026/5/8 16:20:15

Matter协议:打破智能家居生态壁垒,实现跨品牌互联互通

1. 智能家居的“巴别塔”困境与Matter的破局承诺如果你最近在关注智能家居的新闻&#xff0c;或者自己正在折腾家里的智能设备&#xff0c;那么“Matter”这个词你一定不会陌生。从2022年底正式发布到现在&#xff0c;它几乎成了所有行业展会、新品发布会的焦点&#xff0c;被描…

作者头像 李华
网站建设 2026/5/8 16:19:38

Trends MCP:为AI助手注入实时趋势感知的MCP协议数据聚合方案

1. 项目概述&#xff1a;一个为AI大脑注入实时趋势感知的“感官”接口如果你正在使用Claude、Cursor或者任何支持MCP协议的AI助手&#xff0c;并且时常感到它们给出的市场分析、内容建议或产品洞察有点“过时”或“脱离现实”&#xff0c;那么你遇到的正是当前大模型应用的一个…

作者头像 李华
网站建设 2026/5/8 16:19:17

从弗兰肯斯坦到AI芯片:EDA工具如何驾驭“令人恐惧的强大”电力

1. 从青蛙腿到弗兰肯斯坦&#xff1a;一段被遗忘的“电”力往事如果你在半导体或者电子设计自动化&#xff08;EDA&#xff09;这个行当里摸爬滚打有些年头了&#xff0c;大概会对“功耗”这个词又爱又恨。爱的是&#xff0c;每一次工艺节点的跃进&#xff0c;都伴随着性能的飙…

作者头像 李华
网站建设 2026/5/8 16:19:14

苹果手机扣图片换背景用什么工具?2026年最实用的免费抠图方案测评

前段时间&#xff0c;我的朋友圈被一组证件照刷屏了。同样一个人&#xff0c;红色背景、蓝色背景、白色背景&#xff0c;瞬间显得专业度爆表。我当时就在想&#xff0c;这得花多少钱去影楼啊&#xff1f;结果她笑着告诉我&#xff0c;全是用手机自己换的背景。作为一个长期研究…

作者头像 李华