#### 访问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的相关配置: ``` 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`中: ``` @ConfigurationProperties("spring.redis") public class RedisConfiguration { ... @Bean 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操作。基础代码如下: ``` @Component public class RedisService { @Autowired RedisClient redisClient; GenericObjectPool> redisConnectionPool; @PostConstruct public void init() { GenericObjectPoolConfig> poolConfig = new GenericObjectPoolConfig<>(); poolConfig.setMaxTotal(20); poolConfig.setMaxIdle(5); poolConfig.setTestOnReturn(true); poolConfig.setTestWhileIdle(true); this.redisConnectionPool = ConnectionPoolSupport.createGenericObjectPool(() -> redisClient.connect(), poolConfig); } @PreDestroy public void shutdown() { this.redisConnectionPool.close(); this.redisClient.shutdown(); } } ``` 注意到上述代码引入了Commons Pool的一个对象池,用于缓存Redis连接。因为Lettuce本身是基于Netty的异步驱动,在异步访问时并不需要创建连接池,但基于Servlet模型的同步访问时,连接池是有必要的。连接池在`@PostConstruct`方法中初始化,在`@PreDestroy`方法中关闭。 下一步,是在`RedisService`中添加Redis访问方法。为了简化代码,我们仿照`JdbcTemplate.execute(ConnectionCallback)`方法,传入回调函数,可大幅减少样板代码。 首先定义回调函数接口`SyncCommandCallback`: ``` @FunctionalInterface public interface SyncCommandCallback { // 在此操作Redis: T doInConnection(RedisCommands commands); } ``` 编写`executeSync`方法,在该方法中,获取Redis连接,利用callback操作Redis,最后释放连接,并返回操作结果: ``` public T executeSync(SyncCommandCallback callback) { try (StatefulRedisConnection connection = redisConnectionPool.borrowObject()) { connection.setAutoFlushCommands(true); RedisCommands 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 hgetall(String key) { return executeSync(commands -> commands.hgetall(key)); } ``` 常用命令可以提供方法接口,如果要执行任意复杂的操作,就可以通过`executeSync(SyncCommandCallback)`来完成。 完成了`RedisService`后,我们就可以使用Redis了。例如,在`UserController`中,我们在Session中只存放登录用户的ID,用户信息存放到Redis,提供两个方法用于读写: ``` @Controller public class UserController { public static final String KEY_USER_ID = "__userid__"; public static final String KEY_USERS = "__users__"; @Autowired ObjectMapper objectMapper; @Autowired 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: ``` @PostMapping("/signin") public ModelAndView doSignin(@RequestParam("email") String email, @RequestParam("password") 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取出: ``` @GetMapping("/profile") 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 get(String key, Class clazz) { ... } public T set(String key, T value) { ... } ```