在互联网应用追求高并发和高可用的背景下,缓存对于提升程序性能至关重要。相较于本地缓存 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 连接池,可避免频繁创建和销毁连接,提升应用性能和可靠性。
xml<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
-
配置 Redis:在 application.yml 中进行配置,本地配置指定 host 和 port 即可,生产环境配置可参考相关教程。
yamlredis: host: localhost port: 6379 password:
-
启动 Redis 服务 :以
Window
系统为例,使用终端工具启动 Redis 服务,可通过redis-cli ping
检查 Redis 服务是否安装和运行正常,使用redis-server
启动服务,默认端口为 6379。
-
编写测试类 :编写 Redis 测试类
RedisTemplateDemo
,快速验证 Redis 在项目中的可用性。java@SpringBootTest(classes = QuickForumApplication.class) public class RedisTemplateDemo { @Autowired private RedisTemplate<Object, Object> redisTemplate; @Autowired private StringRedisTemplate stringRedisTemplate; @Test public void testPut() { redisTemplate.opsForValue().set("itwanger", "沉默王二"); stringRedisTemplate.opsForList().rightPush("girl", "陈清扬"); stringRedisTemplate.opsForList().rightPush("girl", "小转玲"); stringRedisTemplate.opsForList().rightPush("girl", "茶花女"); } @Test public 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()
获取值。java@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()
获取指定下标范围元素。java@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()
获取指定字段值。java@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()
获取所有元素。java@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()
获取指定下标范围元素。java@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 自动配置机制注入。
java
private static RedisTemplate<String, String> template;
public static void register(RedisTemplate<String, String> template) {
RedisClient.template = template;
}
java
public class ForumCoreAutoConfig {
@Autowired
private ProxyProperties proxyProperties;
public ForumCoreAutoConfig(RedisTemplate<String, String> redisTemplate) {
RedisClient.register(redisTemplate);
}
3.2 用户 session 相关操作
- 验证码校验成功后,调用
RedisClient
的setStrWithExpire
方法存储 session,该方法使用RedisTemplate
的execute
方法,通过RedisCallback
接口的doInRedis
方法执行 Redis 命令,调用RedisConnection
的setEx
方法设置键值对及过期时间。
java
/**
* 带过期时间的缓存写入
*
* @param key
* @param value
* @param expire s为单位
* @return
*/
public static Boolean setStrWithExpire(String key, String value, Long expire) {
return template.execute(new RedisCallback<Boolean>() {
@Override
public Boolean doInRedis(RedisConnection redisConnection) throws DataAccessException {
return redisConnection.setEx(keyBytes(key), expire, valBytes(value));
}
});
}
- 用户登出时,调用
del
方法删除 session。
java
public static void del(String key) {
template.execute((RedisCallback<Long>) con -> con.del(keyBytes(key)));
}
- 用户登录时,调用
getStr
方法根据 session 获取用户 ID。
java
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
。
java
/**
* 使用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验证通过,获取对应的userId
String 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
方法获取哈希表所有字段和值,并进行类型转换后返回。
java
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
方法设置哈希表字段值,返回设置结果。
java
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
方法删除字段,返回删除结果。
java
public static <T> Boolean hDel(String key, String field) {
return template.execute(new RedisCallback<Boolean>() {
@Override
public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
return connection.hDel(keyBytes(key), valBytes(field)) > 0;
}
});
}
- 初始化 sitemap 时,先调用
del
方法,再调用hMSet
方法,hMSet
方法使用RedisTemplate
的execute
方法,通过RedisCallback
接口的doInRedis
方法调用RedisConnection
的hMSet
方法一次性设置多个哈希表字段值。
java
/**
* 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 分刷新站点地图,确保数据一致性。
java
/**
* 采用定时器方案,每天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 连接传递给回调接口执行命令。
java
@Nullable
public <T> T execute(RedisCallback<T> action) {
return this.execute(action, this.isExposeConnection());
}
以下是测试用例示例:
java
@Test
public void testExecute() {
redisTemplate.execute(new RedisCallback<Object>() {
@Override
public 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 脚本等。