当前位置: 首页 > news >正文

【技术派后端篇】技术派中基于 Redis 的缓存实践

在互联网应用追求高并发和高可用的背景下,缓存对于提升程序性能至关重要。相较于本地缓存 Guava Cache 和 Caffeine,Redis 具有显著优势。Redis 支持集群和分布式部署,能横向扩展缓存容量和负载能力,适应大型分布式系统的缓存需求;支持数据持久化存储,可将缓存数据存于磁盘,保障数据不丢失;支持多种数据结构,如字符串、哈希表、列表、集合和有序集合等,提供更灵活的缓存能力;具备主从同步和哨兵机制,实现高可用性和容错能力;还提供丰富的数据处理命令,如排序、聚合、管道和 Lua 脚本等,便于高效处理缓存数据。

Redis 本质上是一个开源的基于内存的 NoSQL 数据库,更适合作为数据库前的缓存层组件。它支持多种数据结构,如 String、List、Set、Hash、ZSet 等,并且支持数据持久化,通过快照和日志将内存数据保存到硬盘,重启后可再次加载使用,其主从复制、哨兵等特性使其成为广受欢迎的缓存中间件。

在 Java 后端开发中,Redis 是面试常考的技术栈之一,是开发四大件(Java 基础、Spring Boot、MySQL、Redis)之一,因此在开发和学习中应扎实掌握 Redis 相关知识。

1 Redis 与 Spring Boot 的整合

  1. 添加依赖:在 pom.xml 中添加 Redis 依赖,Spring Boot 默认使用 Lettuce 作为 Redis 连接池,可避免频繁创建和销毁连接,提升应用性能和可靠性。

    <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
  2. 配置 Redis:在 application.yml 中进行配置,本地配置指定 host 和 port 即可,生产环境配置可参考相关教程。

    redis:host: localhostport: 6379password:
    
  3. 启动 Redis 服务:以Window系统为例,使用终端工具启动 Redis 服务,可通过 redis-cli ping 检查 Redis 服务是否安装和运行正常,使用 redis-server 启动服务,默认端口为 6379。
    在这里插入图片描述

  4. 编写测试类:编写 Redis 测试类 RedisTemplateDemo,快速验证 Redis 在项目中的可用性。

    @SpringBootTest(classes = QuickForumApplication.class)
    public class RedisTemplateDemo {@Autowiredprivate RedisTemplate<Object, Object> redisTemplate;@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Testpublic void testPut() {redisTemplate.opsForValue().set("itwanger", "沉默王二");stringRedisTemplate.opsForList().rightPush("girl", "陈清扬");stringRedisTemplate.opsForList().rightPush("girl", "小转玲");stringRedisTemplate.opsForList().rightPush("girl", "茶花女");}@Testpublic void testGet() {Object value = redisTemplate.opsForValue().get("itwanger");System.out.println(value);List<String> girls = stringRedisTemplate.opsForList().range("girl", 0, -1);System.out.println(girls);}
    }
    

@SpringBootTest(classes = QuickForumApplication.class) 注解指定项目启动类,@Autowired 注解注入 RedisTemplateStringRedisTemplateRedisTemplate 可操作任意类型数据,StringRedisTemplate 仅能操作字符串类型数据。testPut() 方法分别操作 Redis 中的字符串和列表类型数据,testGet() 方法获取相应数据。

2 在 Spring Boot 中使用 Redis 操作不同数据结构

  1. 字符串:注入 RedisTemplate,使用 opsForValue() 方法获取操作对象,通过 set() 设置键值对,get() 获取值。

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;public void set(String key, Object value) {redisTemplate.opsForValue().set(key, value);
    }public Object get(String key) {return redisTemplate.opsForValue().get(key);
    }
    
  2. 列表:注入 StringRedisTemplate,利用 opsForList() 方法获取操作对象,rightPush() 向列表右侧添加元素,range() 获取指定下标范围元素。

    @Autowired
    private StringRedisTemplate stringRedisTemplate;public void push(String key, String value) {stringRedisTemplate.opsForList().rightPush(key, value);
    }public List<String> range(String key, int start, int end) {return stringRedisTemplate.opsForList().range(key, start, end);
    }
    
  3. 哈希:使用 opsForHash() 方法获取操作对象,put() 添加字段和值,get() 获取指定字段值。

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;public void hset(String key, String field, Object value) {redisTemplate.opsForHash().put(key, field, value);
    }public Object hget(String key, String field) {return redisTemplate.opsForHash().get(key, field);
    }
    
  4. 集合:通过 opsForSet() 方法获取操作对象,add() 添加元素,members() 获取所有元素。

    @Autowired
    private StringRedisTemplate stringRedisTemplate;public void sadd(String key, String value) {stringRedisTemplate.opsForSet().add(key, value);
    }public Set<String> smembers(String key) {return stringRedisTemplate.opsForSet().members(key);
    }
    
  5. 有序集合:使用 opsForZSet() 方法获取操作对象,add() 添加元素和分值,range() 获取指定下标范围元素。

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;public void zadd(String key, String value, double score) {redisTemplate.opsForZSet().add(key, value, score);
    }public Set<Object> zrange(String key, long start, long end) {return redisTemplate.opsForZSet().range(key, start, end);
    }
    

3 技术派中的 Redis 实例应用

技术派使用 Redis 缓存用户 session 信息和 sitemap(用于帮助搜索引擎更好索引网站内容的 XML 文件)。

3.1 RedisClient 类

基于 RedisTemplate 封装,简化使用成本。代码路径:com.github.paicoding.forum.core.cache.RedisClient

ForumCoreAutoConfig 配置类通过构造方法注入 RedisTemplate,并调用 RedisClient.register(redisTemplate) 注册到 RedisClient 中,RedisTemplate 由 Spring Boot 自动配置机制注入。

private static RedisTemplate<String, String> template;public static void register(RedisTemplate<String, String> template) {RedisClient.template = template;
}
public class ForumCoreAutoConfig {@Autowiredprivate ProxyProperties proxyProperties;public ForumCoreAutoConfig(RedisTemplate<String, String> redisTemplate) {RedisClient.register(redisTemplate);}

3.2 用户 session 相关操作

  • 验证码校验成功后,调用 RedisClientsetStrWithExpire 方法存储 session,该方法使用 RedisTemplateexecute 方法,通过 RedisCallback 接口的 doInRedis 方法执行 Redis 命令,调用 RedisConnectionsetEx 方法设置键值对及过期时间。
/*** 带过期时间的缓存写入** @param key* @param value* @param expire s为单位* @return*/
public static Boolean setStrWithExpire(String key, String value, Long expire) {return template.execute(new RedisCallback<Boolean>() {@Overridepublic Boolean doInRedis(RedisConnection redisConnection) throws DataAccessException {return redisConnection.setEx(keyBytes(key), expire, valBytes(value));}});
}
  • 用户登出时,调用 del 方法删除 session。
public static void del(String key) {template.execute((RedisCallback<Long>) con -> con.del(keyBytes(key)));
}
  • 用户登录时,调用 getStr 方法根据 session 获取用户 ID。
public static String getStr(String key) {return template.execute((RedisCallback<String>) con -> {byte[] val = con.get(keyBytes(key));return val == null? null : new String(val);});
}

具体调用代码位于com.github.paicoding.forum.service.user.service.help.UserSessionHelper

/*** 使用jwt来存储用户token,则不需要后端来存储session了*/
@Slf4j
@Component
public class UserSessionHelper {@Component@Data@ConfigurationProperties("paicoding.jwt")public static class JwtProperties {/*** 签发人*/private String issuer;/*** 密钥*/private String secret;/*** 有效期,毫秒时间戳*/private Long expire;}private final JwtProperties jwtProperties;private Algorithm algorithm;private JWTVerifier verifier;public UserSessionHelper(JwtProperties jwtProperties) {this.jwtProperties = jwtProperties;algorithm = Algorithm.HMAC256(jwtProperties.getSecret());verifier = JWT.require(algorithm).withIssuer(jwtProperties.getIssuer()).build();}public String genSession(Long userId) {// 1.生成jwt格式的会话,内部持有有效期,用户信息String session = JsonUtil.toStr(MapUtils.create("s", SelfTraceIdGenerator.generate(), "u", userId));String token = JWT.create().withIssuer(jwtProperties.getIssuer()).withExpiresAt(new Date(System.currentTimeMillis() + jwtProperties.getExpire())).withPayload(session).sign(algorithm);// 2.使用jwt生成的token时,后端可以不存储这个session信息, 完全依赖jwt的信息// 但是需要考虑到用户登出,需要主动失效这个token,而jwt本身无状态,所以再这里的redis做一个简单的token -> userId的缓存,用于双重判定RedisClient.setStrWithExpire(token, String.valueOf(userId), jwtProperties.getExpire() / 1000);return token;}public void removeSession(String session) {RedisClient.del(session);}/*** 根据会话获取用户信息** @param session* @return*/public Long getUserIdBySession(String session) {// jwt的校验方式,如果token非法或者过期,则直接验签失败try {DecodedJWT decodedJWT = verifier.verify(session);String pay = new String(Base64Utils.decodeFromString(decodedJWT.getPayload()));// jwt验证通过,获取对应的userIdString userId = String.valueOf(JsonUtil.toObj(pay, HashMap.class).get("u"));// 从redis中获取userId,解决用户登出,后台失效jwt token的问题String user = RedisClient.getStr(session);if (user == null || !Objects.equals(userId, user)) {return null;}return Long.valueOf(user);} catch (Exception e) {log.info("jwt token校验失败! token: {}, msg: {}", session, e.getMessage());return null;}}
}

3.3 sitemap 相关操作

  • 获取 sitemap 时,调用 hGetAll 方法,使用 RedisTemplateexecute 方法执行 Redis 命令,通过 RedisCallback 接口的 doInRedis 方法调用 RedisConnectionhGetAll 方法获取哈希表所有字段和值,并进行类型转换后返回。
    public static <T> Map<String, T> hGetAll(String key, Class<T> clz) {Map<byte[], byte[]> records = template.execute((RedisCallback<Map<byte[], byte[]>>) con -> con.hGetAll(keyBytes(key)));if (records == null) {return Collections.emptyMap();}Map<String, T> result = Maps.newHashMapWithExpectedSize(records.size());for (Map.Entry<byte[], byte[]> entry : records.entrySet()) {if (entry.getKey() == null) {continue;}result.put(new String(entry.getKey()), toObj(entry.getValue(), clz));}return result;}
  • 添加文章时,调用 hSet 方法,使用 RedisTemplateexecute 方法,通过 RedisCallback 接口的 doInRedis 方法,根据值的类型转换为字符串后调用 RedisConnectionhSet 方法设置哈希表字段值,返回设置结果。
public static <T> T hGet(String key, String field, Class<T> clz) {return template.execute((RedisCallback<T>) con -> {byte[] records = con.hGet(keyBytes(key), valBytes(field));if (records == null) {return null;}return toObj(records, clz);});
}
  • 移除文章时,调用 hDel 方法,使用 RedisTemplateexecute 方法,通过 RedisCallback 接口的 doInRedis 方法调用 RedisConnectionhDel 方法删除字段,返回删除结果。
public static <T> Boolean hDel(String key, String field) {return template.execute(new RedisCallback<Boolean>() {@Overridepublic Boolean doInRedis(RedisConnection connection) throws DataAccessException {return connection.hDel(keyBytes(key), valBytes(field)) > 0;}});
}
  • 初始化 sitemap 时,先调用 del 方法,再调用 hMSet 方法,hMSet 方法使用 RedisTemplateexecute 方法,通过 RedisCallback 接口的 doInRedis 方法调用 RedisConnectionhMSet 方法一次性设置多个哈希表字段值。
/*** fixme: 加锁初始化,更推荐的是采用分布式锁*/
private synchronized void initSiteMap() {long lastId = 0L;RedisClient.del(SITE_MAP_CACHE_KEY);while (true) {List<SimpleArticleDTO> list = articleDao.getBaseMapper().listArticlesOrderById(lastId, SCAN_SIZE);// 刷新文章的统计信息list.forEach(s -> countService.refreshArticleStatisticInfo(s.getId()));// 刷新站点地图信息Map<String, Long> map = list.stream().collect(Collectors.toMap(s -> String.valueOf(s.getId()), s -> s.getCreateTime().getTime(), (a, b) -> a));RedisClient.hMSet(SITE_MAP_CACHE_KEY, map);if (list.size() < SCAN_SIZE) {break;}lastId = list.get(list.size() - 1).getId();}
}

为提升搜索引擎对技术派的收录,开发了 sitemap 自动生成工具,在 SitemapServiceImpl 中通过定时任务每天 5:15 分刷新站点地图,确保数据一致性。

 /*** 采用定时器方案,每天5:15分刷新站点地图,确保数据的一致性*/@Scheduled(cron = "0 15 5 * * ?")public void autoRefreshCache() {log.info("开始刷新sitemap.xml的url地址,避免出现数据不一致问题!");refreshSitemap();log.info("刷新完成!");}

4 关于 RedisTemplate 的 execute 方法

RedisTemplateexecute(RedisCallback<T> action) 方法用于执行任意 Redis 命令,接收 RedisCallback 接口作为参数,将 Redis 连接传递给回调接口执行命令。

@Nullable
public <T> T execute(RedisCallback<T> action) {return this.execute(action, this.isExposeConnection());
}

以下是测试用例示例:

@Test
public void testExecute() {redisTemplate.execute(new RedisCallback<Object>() {@Overridepublic Object doInRedis(RedisConnection connection) throws DataAccessException {connection.set("itwanger".getBytes(), "沉默王二".getBytes());byte[] value = connection.get("itwanger".getBytes());String strValue = new String(value);System.out.println(strValue);return null;}});
}

5 总结

Redis 是高性能的内存数据存储系统,支持多种数据结构。在 Spring Boot 中使用 Redis 作为缓存可提升应用性能和响应速度,通过 spring-boot-starter-data-redis 整合 Redis 操作简便。可使用 RedisTemplate 执行各种 Redis 命令,技术派使用 Redis 缓存 session 和 sitemap,应用了字符串和哈希表数据结构。未来将进一步介绍 Redis 其他数据结构和高级功能,如发布/订阅、事务、Lua 脚本等。

6 参考链接

  1. 技术派Redis的缓存示例
  2. 项目仓库(GitHub)
  3. 项目仓库(码云)

相关文章:

  • 分类算法中one-vs-rest策略和one-vs-one 策略的区别是什么?
  • 1.2软考系统架构设计师:系统架构的定义与作用 - 练习题附答案及超详细解析
  • C#+Visual Studio 2022为AutoCAD 2022开发插件并显示在Ribbon选项卡
  • [原理分析]安卓15系统大升级:Doze打盹模式提速50%,续航大幅增强,省电提升率5%
  • 单片机可以用来做机器人吗?
  • 算法之分而治之
  • Unity 场景管理核心教程:从 LoadScene 到 Loading Screen 实战 (Day 35)
  • 配置 VS Code 使用 ESLint 格式化
  • 多模态大语言模型arxiv论文略读(三十二)
  • Linux深度探索:进程管理与系统架构
  • uniapp云打包针对谷歌视频图片权限的解决方案
  • [架构之美]一键服务管理大师:Ubuntu智能服务停止与清理脚本深度解析
  • 《AI大模型应知应会100篇》第30篇:大模型进行数据分析的方法与局限:从实战到边界探索
  • 自定义错误码的必要性
  • Macbook IntelliJ IDEA终端无法运行mvn命令
  • XAML 标记扩展
  • Android端使用无障碍服务实现远程、自动刷短视频
  • 【TeamFlow】4.2 Yew库详细介绍
  • 03-HTML常见元素
  • 衡石科技ChatBI--飞书数据问答机器人配置详解(附具体操作路径和截图)
  • 世界读书日丨这50本书,商务印书馆推荐给教师
  • 纪念沈渭滨︱沈渭滨先生与新修《清史》
  • 85岁眼科专家、武汉大学人民医院原眼科主任喻长泰逝世
  • 安徽省合肥市人大常委会原副主任杜平太接受审查调查
  • 市场监管总局:在全国集中开展食用植物油突出问题排查整治
  • 重庆警方通报“货车轮胎滚进服务区致人死亡”:正进一步调查