共计 5102 个字符,预计需要花费 13 分钟才能阅读完成。
在 Spring Boot 中,要访问 Redis,可以直接引入 spring-boot-starter-data-redis
依赖,它实际上是 Spring Data 的一个子项目——Spring Data Redis,主要用到了这几个组件:
- Lettuce:一个基于 Netty 的高性能 Redis 客户端;
- RedisTemplate:一个类似于 JdbcTemplate 的接口,用于简化 Redis 的操作。
因为 Spring Data Redis 引入的依赖项很多,如果只是为了使用 Redis,完全可以只引入 Lettuce,剩下的操作都自己来完成。
本节我们稍微深入一下 Redis 的客户端,看看怎么一步一步把一个第三方组件引入到 Spring Boot 中。
首先,我们添加必要的几个依赖项:
- io.lettuce:lettuce-core
- org.apache.commons:commons-pool2
注意我们并未指定版本号,因为在 spring-boot-starter-parent
中已经把常用组件的版本号确定下来了。
第一步是在配置文件 application.yml
中添加 Redis 的相关配置:
spring: | |
redis: | |
host: ${REDIS_HOST:localhost} | |
port: ${REDIS_PORT:6379} | |
password: ${REDIS_PASSWORD:} | |
ssl: ${REDIS_SSL:false} | |
database: ${REDIS_DATABASE:0} |
然后,通过 RedisConfiguration
来加载它:
@ConfigurationProperties("spring.redis") | |
public class RedisConfiguration {private String host; | |
private int port; | |
private String password; | |
private int database; | |
// getters and setters... | |
} |
再编写一个 @Bean
方法来创建 RedisClient
,可以直接放在RedisConfiguration
中:
public class RedisConfiguration { | |
... | |
RedisClient redisClient() {RedisURI uri = RedisURI.Builder.redis(this.host, this.port) | |
.withPassword(this.password) | |
.withDatabase(this.database) | |
.build(); | |
return RedisClient.create(uri); | |
} | |
} |
在启动入口引入该配置:
@SpringBootApplication | |
@Import(RedisConfiguration.class) // 加载 Redis 配置 | |
public class Application {...} |
注意:如果在 RedisConfiguration
中标注 @Configuration
,则可通过 Spring Boot 的自动扫描机制自动加载,否则,使用@Import
手动加载。
紧接着,我们用一个 RedisService
来封装所有的 Redis 操作。基础代码如下:
public class RedisService { | |
RedisClient redisClient; | |
GenericObjectPool<StatefulRedisConnection<String, String>> redisConnectionPool; | |
public void init() {GenericObjectPoolConfig<StatefulRedisConnection<String, String>> poolConfig = new GenericObjectPoolConfig<>(); | |
poolConfig.setMaxTotal(20); | |
poolConfig.setMaxIdle(5); | |
poolConfig.setTestOnReturn(true); | |
poolConfig.setTestWhileIdle(true); | |
this.redisConnectionPool = ConnectionPoolSupport.createGenericObjectPool(() -> redisClient.connect(), poolConfig); | |
} | |
public void shutdown() {this.redisConnectionPool.close(); | |
this.redisClient.shutdown();} | |
} |
注意到上述代码引入了 Commons Pool 的一个对象池,用于缓存 Redis 连接。因为 Lettuce 本身是基于 Netty 的异步驱动,在异步访问时并不需要创建连接池,但基于 Servlet 模型的同步访问时,连接池是有必要的。连接池在 @PostConstruct
方法中初始化,在 @PreDestroy
方法中关闭。
下一步,是在 RedisService
中添加 Redis 访问方法。为了简化代码,我们仿照 JdbcTemplate.execute(ConnectionCallback)
方法,传入回调函数,可大幅减少样板代码。
首先定义回调函数接口SyncCommandCallback
:
public interface SyncCommandCallback<T> {// 在此操作 Redis: | |
T doInConnection(RedisCommands<String, String> commands); | |
} |
编写 executeSync
方法,在该方法中,获取 Redis 连接,利用 callback 操作 Redis,最后释放连接,并返回操作结果:
public <T> T executeSync(SyncCommandCallback<T> callback) {try (StatefulRedisConnection<String, String> connection = redisConnectionPool.borrowObject()) {connection.setAutoFlushCommands(true); | |
RedisCommands<String, String> commands = connection.sync(); | |
return callback.doInConnection(commands); | |
} catch (Exception e) {logger.warn("executeSync redis failed.", e); | |
throw new RuntimeException(e); | |
} | |
} |
有的童鞋觉得这样访问 Redis 的代码太复杂了,实际上我们可以针对常用操作把它封装一下,例如 set
和get
命令:
public String set(String key, String value) {return executeSync(commands -> commands.set(key, value)); | |
} | |
public String get(String key) {return executeSync(commands -> commands.get(key)); | |
} |
类似的,hget
和 hset
操作如下:
public boolean hset(String key, String field, String value) {return executeSync(commands -> commands.hset(key, field, value)); | |
} | |
public String hget(String key, String field) {return executeSync(commands -> commands.hget(key, field)); | |
} | |
public Map<String, String> hgetall(String key) {return executeSync(commands -> commands.hgetall(key)); | |
} |
常用命令可以提供方法接口,如果要执行任意复杂的操作,就可以通过 executeSync(SyncCommandCallback<T>)
来完成。
完成了 RedisService
后,我们就可以使用 Redis 了。例如,在 UserController
中,我们在 Session 中只存放登录用户的 ID,用户信息存放到 Redis,提供两个方法用于读写:
public class UserController {public static final String KEY_USER_ID = "__userid__"; | |
public static final String KEY_USERS = "__users__"; | |
ObjectMapper objectMapper; | |
RedisService redisService; | |
// 把 User 写入 Redis: | |
private void putUserIntoRedis(User user) throws Exception {redisService.hset(KEY_USERS, user.getId().toString(), objectMapper.writeValueAsString(user)); | |
} | |
// 从 Redis 读取 User: | |
private User getUserFromRedis(HttpSession session) throws Exception {Long id = (Long) session.getAttribute(KEY_USER_ID); | |
if (id != null) {String s = redisService.hget(KEY_USERS, id.toString()); | |
if (s != null) {return objectMapper.readValue(s, User.class); | |
} | |
} | |
return null; | |
} | |
... | |
} |
用户登录成功后,把 ID 放入 Session,把 User
实例放入 Redis:
public ModelAndView doSignin( String email, String password, HttpSession session)throws Exception {try {User user = userService.signin(email, password); | |
session.setAttribute(KEY_USER_ID, user.getId()); | |
putUserIntoRedis(user); | |
} catch (RuntimeException e) {return new ModelAndView("signin.html", Map.of("email", email, "error", "Signin failed")); | |
} | |
return new ModelAndView("redirect:/profile"); | |
} |
需要获取 User
时,从 Redis 取出:
public ModelAndView profile(HttpSession session) throws Exception {User user = getUserFromRedis(session); | |
if (user == null) {return new ModelAndView("redirect:/signin"); | |
} | |
return new ModelAndView("profile.html", Map.of("user", user)); | |
} |
从 Redis 读写 Java 对象时,序列化和反序列化是应用程序的工作,上述代码使用 JSON 作为序列化方案,简单可靠。也可将相关序列化操作封装到 RedisService
中,这样可以提供更加通用的方法:
public <T> T get(String key, Class<T> clazz) {...} | |
public <T> T set(String key, T value) {...} |
练习
在 Spring Boot 中访问 Redis。
下载练习
小结
Spring Boot 默认使用 Lettuce 作为 Redis 客户端,同步使用时,应通过连接池提高效率。
