在Spring Boot中,要访问Redis,可以直接引入spring-boot-starter-data-redis
依赖,它实际上是Spring Data的一个子项目——Spring Data Redis,主要用到了这几个组件:
因为Spring Data Redis引入的依赖项很多,如果只是为了使用Redis,完全可以只引入Lettuce,剩下的操作都自己来完成。
本节我们稍微深入一下Redis的客户端,看看怎么一步一步把一个第三方组件引入到Spring Boot中。
首先,我们添加必要的几个依赖项:
注意我们并未指定版本号,因为在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<StatefulRedisConnection<String, String>> redisConnectionPool;
@PostConstruct
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);
}
@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<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,提供两个方法用于读写:
@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> T get(String key, Class<T> clazz) {
...
}
public <T> T set(String key, T value) {
...
}