前边我们已经学习了 Redis 的一些基本命令,以及通过 Jedis、Lettuce 来操作 Redis,但在实际的开发中,我们更多的会在 SpringBoot 中整合 Redis,来提高开发效率。
一、集成 Redis
我这里使用 SpringBoot 2.5.0版本,通过 Spring Data Redis 来集成 Redis:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
然后就是一些 Redis 的配置:
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.password=shehuan
从 SpringBoot2.x 开始,默认使用 Lettuce 作为 Spring Data Redis 的内部实现,而不是 Jedis,这一点可以从spring-boot-starter-data-redis
的 pom 文件看出:
如果需要使用 Jedis,则需要手动添加对应的依赖:
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.6.0</version>
</dependency>
并在配置文件中切换 Redis 客户端为 jedis:
spring.redis.client-type=jedis
最基本的配置就这些了,根据 SpringBoot 的自动装配机制,会自动的创建一些对象来方便我们操作 Redis:
-
RedisConnectionFactory
,就是根据指定的配置来获取 Redis 连接的 -
RedisTemplate
、StringRedisTemplate
,用来操作 Redis 存取数据的,既然这两个都是用来存取数据的,那肯定是有区别的,下边我们具体看一下。
二、RedisTemplate
在 Redis 中,StringRedisTemplate
是专门用来存、取字符串类型数据的,它继承RedisTemplate
,使用StringRedisSerializer
作为序列化器。
RedisTemplate
可以用来存、取自定义的复杂数据类型,当然也包括字符串类型,它默认使用JdkSerializationRedisSerializer
作为 Redis 中 key、value 的序列化器,但是这个序列化器会先将 key、value 序列化成字节数组然后再存储到 Redis,导致无法通过 Redis 客户端直观的看出到底存储的是什么信息,有问题也就不好排查了,例如我们存储一个User
对象:
public class User implements Serializable {
private Interger id;
private String name;
private Integer age;
public User() {
}
public User(Integer id, String name, Integer age) {
this.id = id;
this.name = name;
this.age = age;
}
// 省略get、set
}
@Service
public class MyRedisService {
@Autowired
RedisTemplate<Object, Object> redisTemplate;
public void test1() {
redisTemplate.opsForValue().set("user", new User("zhangsan", 18));
}
}
通过测试类执行程序:
@SpringBootTest
public class MyRedisApplicationTests {
@Autowired
MyRedisService myRedisService;
@Test
void contextLoads() {
myRedisService.test1();
}
}
然后在客户端查看数据,红色区域分别是存进去的 key、value:
为了解决这个问题,一般需要我们自定义RedisTemplate
来覆盖框架生成的。设置 key 的序列化器为StringRedisSerializer
,即将 key 序列化为字符串;至于 value 的序列化器可以使用默认的JdkSerializationRedisSerializer
,也可以设置为Jackson2JsonRedisSerializer
,即将 value 序列化为 json 字符串在存储。由于 Redis 中 Hash 类型数据结构的 value 也是一个 field-value 键值对,也可以分别指定序列化器。
@Configuration
public class RedisConfig {
@Bean("redisTemplate")
public RedisTemplate<Object, Object> initRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
// 设置连接工厂
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 定义 String 序列化器
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// 定义 Jackson 序列化器
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
//反序列化时智能识别变量名(识别没有按驼峰格式命名的变量名)
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
//反序列化识别对象类型
// objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.WRAPPER_ARRAY);
//反序列化如果有多的属性,不抛出异常
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
//反序列化如果碰到不识别的枚举值,是否作为空值解释,true:不会抛不识别的异常, 会赋空值,false:会抛不识别的异常
objectMapper.configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL, true);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
// 设置 Redis 的 key 以及 hash 结构的 field 使用 String 序列化器
redisTemplate.setKeySerializer(stringRedisSerializer);
redisTemplate.setHashKeySerializer(stringRedisSerializer);
// 设置 Redis 的 value 以及 hash 结构的 value 使用 Jackson 序列化器
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
清空数据,再次运行测试代码后,再查看客户端数据,基本符合预期了:
注意,RedisTemplate
使用JdkSerializationRedisSerializer
作为 value 的默认序列化器,直接存储数字或者以字符串形式存储数字,后期都是无法使用 Redis 命令对 value 进行各种数学运算的;使用Jackson2JsonRedisSerializer
作为 value 的序列化器时直接存储数字,是可以对value 进行数学运算的;StringRedisTemplate
使用StringRedisSerializer
作为默认的序列化器,以字符串形式存储的数字后期是可以进行数学运算的。
三、操作 Redis 数据类型
如果要存取的数据可以用字符串类型表示,建议使用StringRedisTemplate
,如果是自定义的复杂对象可以使用RedisTemplate
,这里的RedisTemplate
是前边我们自定义的。
Spring Data Redis 中提供了如下接口,可以完成对 Redis 常见数据结构的操作:
-
ValueOperations
,对应 String 数据类型,bit(bitmap/位图)操作也是用它实现 -
ListOperations
,对应 List 数据类型 -
SetOperations
,对应 Set 数据类型 -
HashOperations
,对应 Hash 数据类型 -
ZSetOperations
,对应 ZSet 数据类型 -
GeoOperations
,对应 Geo 数据类型 -
HyperLogLogOperations
,对应 HyperLogLog 数据类型
具体的用法也是很简单的,和 Redis 数据类型的用法基本一致,下边举几个例子:
@Service
public class MyRedisService {
@Autowired
StringRedisTemplate stringRedisTemplate;
@Autowired
RedisTemplate<Object, Object> redisTemplate;
public void test2() {
stringRedisTemplate.opsForValue().set("key1", "10");
String key1 = stringRedisTemplate.opsForValue().get("key1");
stringRedisTemplate.opsForSet().add("key2", "1", "2", "3");
Boolean isMember = stringRedisTemplate.opsForSet().isMember("key2", "1");
redisTemplate.opsForList().leftPush("user", new User("zhangsan", 18));
redisTemplate.opsForList().leftPush("user", new User("lisi", 20));
User user1 = (User) redisTemplate.opsForList().rightPop("user");
}
}
使用XxxxOperations
系列的接口,如果要对一个 key 的值进行多次操作,就需要多次绑定同一个 key,会麻烦一些。
针对这种情况,我们可以使用BoundKeyOperations
接口的实现类来实现对一个 key 的值进行多次操作:
BoundValueOperations
BoundListOperations
BoundSetOperations
BoundHashOperations
BoundZSetOperations
BoundGeoOperations
public void test3() {
BoundValueOperations<String, String> boundValueOperations = stringRedisTemplate.boundValueOps("key1");
boundValueOperations.set("10");
String key1 = boundValueOperations.get();
BoundSetOperations<String, String> boundSetOperations = stringRedisTemplate.boundSetOps("key2");
boundSetOperations.add("1", "2", "3");
Boolean isMember = boundSetOperations.isMember("1");
BoundListOperations<Object, Object> boundListOperations = redisTemplate.boundListOps("user");
boundListOperations.leftPush(new User("zhangsan", 18));
boundListOperations.leftPush(new User("lisi", 20));
User user1 = (User) boundListOperations.rightPop();
}
四、事务
之前的文章我们已经知道,事务中常用的命令有watch
、unwatch
、multi
、exec
,在 SpringBoot 也是类似的,但由于事务中往往涉及多个命令,我们要保证在同一个连接中执行所有的命令,这时需要用到SessionCallback
接口,之前文章中我们用 Jedis 实现了事务,这里我们在原例子基础上修改为 SpringBoot 整合 Redis 后事务的用法:
@Service
public class MyRedisService {
@Autowired
StringRedisTemplate stringRedisTemplate;
public void test4() {
// 设置商品库存为1000件
stringRedisTemplate.opsForValue().set("stock", "1");
List<Object> results = stringRedisTemplate.execute(new SessionCallback<List<Object>>() {
@Override
public List<Object> execute(RedisOperations operations) throws DataAccessException {
// 监控库存
operations.watch("stock");
// 获取库存
int stock = Integer.parseInt(String.valueOf(operations.opsForValue().get("stock")));
// 如果库存大于购买数量
if (stock > 10) {
stock = stock - 10;
} else {
// 取消监控
operations.unwatch();
return null;
}
// 开启事务
operations.multi();
//减扣库存
operations.opsForValue().set("stock", String.valueOf(stock));
// 执行事务,此处打断点,在客户端修改库存
List<Object> results = operations.exec();
// 如果事务执行过程中发现库存在其它地方被修改过,则返回List的大小为0
return results;
}
});
if (results == null || results.size() == 0) {
System.out.println("库存减扣失败!");
} else {
System.out.println("剩余库存:" + stringRedisTemplate.opsForValue().get("stock"));
}
}
}
五、pipeline
前边这些例子中,Redis 命令都是逐条发送到服务器去执行的,这是 Redis 的默认策略,如果有大量的命令需要执行,这样效率显然是不高的,许多时间都会耗费在网络传输上。基于这样的情况,我们可以使用pipeline
技术来优化,将指令批量发送到服务器去执行,提高效率。具体的用法如下:
@Service
public class MyRedisService {
@Autowired
StringRedisTemplate stringRedisTemplate;
public void test5() {
stringRedisTemplate.executePipelined(new SessionCallback<Object>() {
@Override
public <K, V> Object execute(RedisOperations<K, V> operations) throws DataAccessException {
// 写在这里的命令会被批量发送到服务器执行
return null;
}
});
}
}
六、缓存
一般数据库操作都是效率比较低的,容易产生性能问题,可以将一些从数据库查出的数据缓存起来,重复利用,提高性能。在 Sping3.1 中引入了缓存(Cache)的功能,Sping 的缓存功能支持多种实现,Redis 是比较常用的,还有Ehcache
等其它的,这里就不介绍了,用法基本一致。SpringBoot 整合 Redis 后,可以很方便的用 Redis 作为缓存的实现方式,实现数据的缓存。
除了上边 Redis 连接相关的配置外,还需要额外添加使用 Redis 作为缓存需要的配置:
# 指定缓存类型
spring.cache.type=redis
# 缓存超时时间,0为永不超时
spring.cache.redis.time-to-live=0ms
以及 Spring 缓存的依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
在 SpringBoot 启动类上添加开启缓存的注解@EnableCaching
:
@SpringBootApplication
@EnableCaching
public class MyRedisApplication {
public static void main(String[] args) {
SpringApplication.run(MyRedisApplication.class, args);
}
}
接下来就是如何去缓存数据了,这里涉及到如下几个注解:
-
@CacheConfig
,在类上使用,表示该类中方法使用的缓存名称(可以理解为数据缓存的命名空间),除了在类上使用该注解配置缓存名称,还可以用下边三个注解在方法上配置 -
@CachePut
,一般用在新增或更新业务的方法上,当数据新增或更新成功后,将方法的返回结果使用指定的 key 添加到缓存中,或更新缓存中已有的 key 的值 -
@Cacheable
,一般用在查询业务的方法上,先从缓存中根据指定的 key 查询数据,如果查询到就直接返回,否则执行该方法来获取数据,最后将方法的返回结果保存到缓存 -
@CacheEvict
,一般用在删除业务的方法上,默认会在方法执行结束后移除指定 key 对应的缓存数据
下边用一个例子具体看如何使用这些注解:
@Service
@CacheConfig(cacheNames = "cache1")
public class UserService {
@Autowired
UserDao userDao;
@Cacheable(cacheNames = "cache2", key = "'user'+#id")
public User getUserById(String id) {
return userDao.getUserById(id);
}
@CachePut(key = "'user'+#user.id")
public User addUser(User user) {
return userDao.addUser(user);
}
@CachePut(key = "'user'+#user.id", condition = "#result != 'null'")
public User updateUser(User user) {
if (userDao.getUserById(user.getId()) == null) {
return null;
}
return userDao.updateUser(user);
}
@CacheEvict(key = "'user'+#id")
public Integer deleteUserById(String id) {
return userDao.deleteUserById(id);
}
}
针对这个例子做一些说明:
-
UserDao
是用来模拟数据库操作的,里边的内容不重要。 - 注解的
cacheNames
属性用来配置缓存的名称,方法上的配置会覆盖类上的配置。 -
@Cacheable
、@CachePut
、@CacheEvict
都配置了一个 key 属性,作为 Redis 中缓存数据的 key,key 的值是通过一个 Spring EL 表达式返回的,这样可以根据实际需求自由的指定 key 的值。 -
@CachePut
还配置了一个condition
属性,用作条判断,这里表示方法返回的结果不为 null 才缓存数据。当然你也可以在@Cacheable
、@CacheEvict
中condition
属性,以便在满足对应条件时才对缓存做相应操作。
最后做一个简单的测试:
@SpringBootTest
class MyRedisApplicationTests {
@Autowired
UserService userService;
@Test
void contextLoads() {
// 查询用户
userService.getUserById(100);
// 添加用户
User user = new User(102, "wangwu", 19);
userService.addUser(user);
}
}
在 Redis 客户端查看缓存的数据: