访问Redis

在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的相关配置:

  1. spring:
  2. redis:
  3. host: ${REDIS_HOST:localhost}
  4. port: ${REDIS_PORT:6379}
  5. password: ${REDIS_PASSWORD:}
  6. ssl: ${REDIS_SSL:false}
  7. database: ${REDIS_DATABASE:0}

然后,通过RedisConfiguration来加载它:

  1. @ConfigurationProperties("spring.redis")
  2. public class RedisConfiguration {
  3. private String host;
  4. private int port;
  5. private String password;
  6. private int database;
  7. // getters and setters...
  8. }

再编写一个@Bean方法来创建RedisClient,可以直接放在RedisConfiguration中:

  1. @ConfigurationProperties("spring.redis")
  2. public class RedisConfiguration {
  3. ...
  4. @Bean
  5. RedisClient redisClient() {
  6. RedisURI uri = RedisURI.Builder.redis(this.host, this.port)
  7. .withPassword(this.password)
  8. .withDatabase(this.database)
  9. .build();
  10. return RedisClient.create(uri);
  11. }
  12. }

在启动入口引入该配置:

  1. @SpringBootApplication
  2. @Import(RedisConfiguration.class) // 加载Redis配置
  3. public class Application {
  4. ...
  5. }

注意:如果在RedisConfiguration中标注@Configuration,则可通过Spring Boot的自动扫描机制自动加载,否则,使用@Import手动加载。

紧接着,我们用一个RedisService来封装所有的Redis操作。基础代码如下:

  1. @Component
  2. public class RedisService {
  3. @Autowired
  4. RedisClient redisClient;
  5. GenericObjectPool<StatefulRedisConnection<String, String>> redisConnectionPool;
  6. @PostConstruct
  7. public void init() {
  8. GenericObjectPoolConfig<StatefulRedisConnection<String, String>> poolConfig = new GenericObjectPoolConfig<>();
  9. poolConfig.setMaxTotal(20);
  10. poolConfig.setMaxIdle(5);
  11. poolConfig.setTestOnReturn(true);
  12. poolConfig.setTestWhileIdle(true);
  13. this.redisConnectionPool = ConnectionPoolSupport.createGenericObjectPool(() -> redisClient.connect(), poolConfig);
  14. }
  15. @PreDestroy
  16. public void shutdown() {
  17. this.redisConnectionPool.close();
  18. this.redisClient.shutdown();
  19. }
  20. }

注意到上述代码引入了Commons Pool的一个对象池,用于缓存Redis连接。因为Lettuce本身是基于Netty的异步驱动,在异步访问时并不需要创建连接池,但基于Servlet模型的同步访问时,连接池是有必要的。连接池在@PostConstruct方法中初始化,在@PreDestroy方法中关闭。

下一步,是在RedisService中添加Redis访问方法。为了简化代码,我们仿照JdbcTemplate.execute(ConnectionCallback)方法,传入回调函数,可大幅减少样板代码。

首先定义回调函数接口SyncCommandCallback

  1. @FunctionalInterface
  2. public interface SyncCommandCallback<T> {
  3. // 在此操作Redis:
  4. T doInConnection(RedisCommands<String, String> commands);
  5. }

编写executeSync方法,在该方法中,获取Redis连接,利用callback操作Redis,最后释放连接,并返回操作结果:

  1. public <T> T executeSync(SyncCommandCallback<T> callback) {
  2. try (StatefulRedisConnection<String, String> connection = redisConnectionPool.borrowObject()) {
  3. connection.setAutoFlushCommands(true);
  4. RedisCommands<String, String> commands = connection.sync();
  5. return callback.doInConnection(commands);
  6. } catch (Exception e) {
  7. logger.warn("executeSync redis failed.", e);
  8. throw new RuntimeException(e);
  9. }
  10. }

有的童鞋觉得这样访问Redis的代码太复杂了,实际上我们可以针对常用操作把它封装一下,例如setget命令:

  1. public String set(String key, String value) {
  2. return executeSync(commands -> commands.set(key, value));
  3. }
  4. public String get(String key) {
  5. return executeSync(commands -> commands.get(key));
  6. }

类似的,hgethset操作如下:

  1. public boolean hset(String key, String field, String value) {
  2. return executeSync(commands -> commands.hset(key, field, value));
  3. }
  4. public String hget(String key, String field) {
  5. return executeSync(commands -> commands.hget(key, field));
  6. }
  7. public Map<String, String> hgetall(String key) {
  8. return executeSync(commands -> commands.hgetall(key));
  9. }

常用命令可以提供方法接口,如果要执行任意复杂的操作,就可以通过executeSync(SyncCommandCallback<T>)来完成。

完成了RedisService后,我们就可以使用Redis了。例如,在UserController中,我们在Session中只存放登录用户的ID,用户信息存放到Redis,提供两个方法用于读写:

  1. @Controller
  2. public class UserController {
  3. public static final String KEY_USER_ID = "__userid__";
  4. public static final String KEY_USERS = "__users__";
  5. @Autowired ObjectMapper objectMapper;
  6. @Autowired RedisService redisService;
  7. // 把User写入Redis:
  8. private void putUserIntoRedis(User user) throws Exception {
  9. redisService.hset(KEY_USERS, user.getId().toString(), objectMapper.writeValueAsString(user));
  10. }
  11. // 从Redis读取User:
  12. private User getUserFromRedis(HttpSession session) throws Exception {
  13. Long id = (Long) session.getAttribute(KEY_USER_ID);
  14. if (id != null) {
  15. String s = redisService.hget(KEY_USERS, id.toString());
  16. if (s != null) {
  17. return objectMapper.readValue(s, User.class);
  18. }
  19. }
  20. return null;
  21. }
  22. ...
  23. }

用户登录成功后,把ID放入Session,把User实例放入Redis:

  1. @PostMapping("/signin")
  2. public ModelAndView doSignin(@RequestParam("email") String email, @RequestParam("password") String password, HttpSession session) throws Exception {
  3. try {
  4. User user = userService.signin(email, password);
  5. session.setAttribute(KEY_USER_ID, user.getId());
  6. putUserIntoRedis(user);
  7. } catch (RuntimeException e) {
  8. return new ModelAndView("signin.html", Map.of("email", email, "error", "Signin failed"));
  9. }
  10. return new ModelAndView("redirect:/profile");
  11. }

需要获取User时,从Redis取出:

  1. @GetMapping("/profile")
  2. public ModelAndView profile(HttpSession session) throws Exception {
  3. User user = getUserFromRedis(session);
  4. if (user == null) {
  5. return new ModelAndView("redirect:/signin");
  6. }
  7. return new ModelAndView("profile.html", Map.of("user", user));
  8. }

从Redis读写Java对象时,序列化和反序列化是应用程序的工作,上述代码使用JSON作为序列化方案,简单可靠。也可将相关序列化操作封装到RedisService中,这样可以提供更加通用的方法:

  1. public <T> T get(String key, Class<T> clazz) {
  2. ...
  3. }
  4. public <T> T set(String key, T value) {
  5. ...
  6. }

练习

访问Redis - 图1下载练习:在Spring Boot中访问Redis (推荐使用IDE练习插件快速下载)

小结

Spring Boot默认使用Lettuce作为Redis客户端,同步使用时,应通过连接池提高效率。

读后有收获可以支付宝请作者喝咖啡,读后有疑问请加微信群讨论:

访问Redis - 图2 访问Redis - 图3