亿级流量点赞系统第二章

项目地址

GitHub地址💥:https://github.com/tenyon61/likeboom

后端初始化模板💥:https://github.com/tenyon61/springboot3-demo/tree/single

快速开始

✨引入Sa-Token

xml
复制代码
<properties> <sa-token.version>1.42.0</sa-token.version> </properties> <dependency> <groupId>cn.dev33</groupId> <artifactId>sa-token-spring-boot3-starter</artifactId> <version>${sa-token.version}</version> </dependency> <dependency> <groupId>cn.dev33</groupId> <artifactId>sa-token-redis-jackson</artifactId> <version>${sa-token.version}</version> <exclusions> <exclusion> <groupId>io.lettuce</groupId> <artifactId>lettuce-core</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </dependency>

✨Redis配置

没有用到SpringSession,无须配置其序列化

java
复制代码
@Configuration public class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(connectionFactory); // 使用 Jackson2JsonRedisSerializer 序列化值 ObjectMapper objectMapper = new ObjectMapper(); objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL); Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(objectMapper, Object.class); // Key 使用 String 序列化 template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(serializer); template.setHashKeySerializer(new StringRedisSerializer()); template.setHashValueSerializer(serializer); template.afterPropertiesSet(); return template; } // @Bean public RedisSerializer<Object> springSessionDefaultRedisSerializer() { // 让 Spring Session 使用 JSON 方式存储 return new GenericJackson2JsonRedisSerializer(); } }

同时在application.yml中配置Sa-Token

yaml
复制代码
sa-token: # token 名称(同时也是 cookie 名称) token-name: satoken # token 有效期(单位:秒) 默认30天,-1 代表永久有效 timeout: 2592000 # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 active-timeout: -1 # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) is-concurrent: true # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) is-share: true # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) token-style: uuid # 是否输出操作日志 is-log: true

✨冷热分离策略实现

存入redis的值加上过期时间30day

java
复制代码
@Schema(description = "热点点赞数据") @Data public class HotThumb implements Serializable { @Schema(description = "点赞Id") private Long thumbId; @Schema(description = "过期时间ms") private Long expireTime; @Serial private static final long serialVersionUID = 1L; }

✨是否点赞逻辑

流程如下

  1. 根据 thumb:userId,blogId查询 Redis是否有缓存的点赞数据
  2. Redis中没有就查 MySQL中是否有数据
  3. Redis中有数据就看是否过期
java
复制代码
@Override public Boolean hasThumb(Long blogId, Long userId) { HotThumb hotThumb = (HotThumb) redisTemplate.opsForHash().get(RedisConstant.USER_THUMB_KEY_PREFIX + userId.toString(), blogId.toString()); // 查看 redis中的点赞缓存数据 if (hotThumb == null) { //查看 mysql中的点赞数据 Thumb thumb = this.lambdaQuery().eq(Thumb::getUserId, userId).eq(Thumb::getBlogId, blogId).one(); return thumb != null; } if (hotThumb.getExpireTime() < DateUtil.current()) { // 点赞数据过期 redisTemplate.opsForHash().delete(RedisConstant.USER_THUMB_KEY_PREFIX + userId, blogId.toString()); return false; } return true; }

✨点赞逻辑

点赞操作时候实现点赞数据缓存1个月,用hutool工具类生成时间戳LocalDateTimeUtil.now().plusDays(30).toInstant(ZoneOffset.ofHours(8)).toEpochMilli()

java
复制代码
@Override public Boolean doThumb(DoThumbDTO doThumbDTO) { if (doThumbDTO == null || doThumbDTO.getBlogId() == null) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } User loginUser = userService.getLoginUser(); // 加锁 synchronized (loginUser.getId().toString().intern()) { // 编程式事务 return transactionTemplate.execute(status -> { Long blogId = doThumbDTO.getBlogId(); boolean exists = hasThumb(blogId, loginUser.getId()); if (exists) { throw new BusinessException(ErrorCode.OPERATION_ERROR, "用户已点赞"); } boolean update = blogService.lambdaUpdate() .eq(Blog::getId, blogId) .setSql("thumbCount = thumbCount + 1") .update(); Thumb thumb = new Thumb(); thumb.setUserId(loginUser.getId()); thumb.setBlogId(blogId); // 更新成功才执行 boolean success = update && this.save(thumb); if (success) { HotThumb hotThumb = new HotThumb(); hotThumb.setThumbId(thumb.getId()); // 30天热点点赞数据缓存 hotThumb.setExpireTime(LocalDateTimeUtil.now().plusDays(30).toInstant(ZoneOffset.ofHours(8)).toEpochMilli()); redisTemplate.opsForHash().put(RedisConstant.USER_THUMB_KEY_PREFIX + loginUser.getId().toString(), blogId.toString(), hotThumb); } return true; }); } }

✨取消点赞逻辑

先查询Redis中有缓存数据,没有就去查询 MySQL,这里不用上面hasThumb方法,下面自会删除缓存数据

java
复制代码
Long blogId = doThumbDTO.getBlogId(); HotThumb hotThumb = (HotThumb) redisTemplate.opsForHash().get(RedisConstant.USER_THUMB_KEY_PREFIX + loginUser.getId().toString(),blogId.toString()); // redis中无点赞数据,或者已经过期就要去查询 mysql if (hotThumb == null || hotThumb.getExpireTime() < DateUtil.current()) { Thumb thumb = this.lambdaQuery().eq(Thumb::getUserId, loginUser.getId()).eq(Thumb::getBlogId, blogId).one(); ThrowUtils.throwIf(thumb == null, ErrorCode.OPERATION_ERROR, "用户未点赞"); hotThumb = new HotThumb(); hotThumb.setThumbId(thumb.getId()); }

删除缓存数据

java
复制代码
boolean success = update && this.removeById(hotThumb.getThumbId()); if (success) { redisTemplate.opsForHash().delete(RedisConstant.USER_THUMB_KEY_PREFIX + loginUser.getId().toString(),blogId.toString()); }

最后结果是当前用户没点赞 Redis中的缓存数据和 MySQL中的数据全部删除

✨获取博客列表

需要将thumbList从 MySQL中读取改为从 Redis中读取

java
复制代码
List<Object> blogIds = blogList.stream().map(blog -> blog.getId().toString()).collect(Collectors.toList()); Map<Long, Boolean> blogIdHasThumbMap = new HashMap<>(); if (StpUtil.isLogin()) { User user = (User) StpUtil.getSession().get(UserConstant.USER_LOGIN_STATE); List<Object> thumbList = redisTemplate.opsForHash().multiGet(RedisConstant.USER_THUMB_KEY_PREFIX + user.getId().toString(), blogIds); for (int i = 0; i < thumbList.size(); i++) { if (thumbList.get(i) != null) { blogIdHasThumbMap.put(Long.valueOf(blogIds.get(i).toString()), true); } } }

✨验证数据

  • 点赞

1

  • 查看单个博客

2

  • 查看所有博客列表

3

本章总结

  • 用Sa-Token对用户进行登录鉴权,简单高效
  • 将点赞信息由 MySQL 转存 redis 的 Hash,通过用户 Id批量获取用户点赞博客列表
  • 对点赞数据设置过期1月周长,实现数据冷热分离

项目踩坑

  • 先将之前thumb表数据删除,避免数据库主键冲突
0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
下载 APP