【技术派后端篇】技术派中基于 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 的整合
-
添加依赖:在 pom.xml 中添加 Redis 依赖,Spring Boot 默认使用 Lettuce 作为 Redis 连接池,可避免频繁创建和销毁连接,提升应用性能和可靠性。
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
-
配置 Redis:在 application.yml 中进行配置,本地配置指定 host 和 port 即可,生产环境配置可参考相关教程。
redis:host: localhostport: 6379password:
-
启动 Redis 服务:以
Window
系统为例,使用终端工具启动 Redis 服务,可通过redis-cli ping
检查 Redis 服务是否安装和运行正常,使用redis-server
启动服务,默认端口为 6379。
-
编写测试类:编写 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
注解注入 RedisTemplate
和 StringRedisTemplate
。RedisTemplate
可操作任意类型数据,StringRedisTemplate
仅能操作字符串类型数据。testPut()
方法分别操作 Redis 中的字符串和列表类型数据,testGet()
方法获取相应数据。
2 在 Spring Boot 中使用 Redis 操作不同数据结构
-
字符串:注入
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); }
-
列表:注入
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); }
-
哈希:使用
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); }
-
集合:通过
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); }
-
有序集合:使用
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 相关操作
- 验证码校验成功后,调用
RedisClient
的setStrWithExpire
方法存储 session,该方法使用RedisTemplate
的execute
方法,通过RedisCallback
接口的doInRedis
方法执行 Redis 命令,调用RedisConnection
的setEx
方法设置键值对及过期时间。
/*** 带过期时间的缓存写入** @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
方法,使用RedisTemplate
的execute
方法执行 Redis 命令,通过RedisCallback
接口的doInRedis
方法调用RedisConnection
的hGetAll
方法获取哈希表所有字段和值,并进行类型转换后返回。
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
方法,使用RedisTemplate
的execute
方法,通过RedisCallback
接口的doInRedis
方法,根据值的类型转换为字符串后调用RedisConnection
的hSet
方法设置哈希表字段值,返回设置结果。
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
方法,使用RedisTemplate
的execute
方法,通过RedisCallback
接口的doInRedis
方法调用RedisConnection
的hDel
方法删除字段,返回删除结果。
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
方法使用RedisTemplate
的execute
方法,通过RedisCallback
接口的doInRedis
方法调用RedisConnection
的hMSet
方法一次性设置多个哈希表字段值。
/*** 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 方法
RedisTemplate
的 execute(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 参考链接
- 技术派Redis的缓存示例
- 项目仓库(GitHub)
- 项目仓库(码云)