Redis 介绍
- Redis 是基于内存的 K-V 数据库
- Redis 的速度很快,支持每秒十万级的 GET 和万级的 SET
- 基于内存
- IO 多路复用
- 单线程,开销小
- Redis 支持 String/List/Set/ZSet/Hash 五种基本数据类型
- Redis 有丰富的客户端类库
- Jedis,小巧而功能完善
- Lettuce,高级的 Redis 客户端,支持集群
- Redisson,功能强大的分布式中间件
集成 Redis
-
引入 maven 依赖
<!--redis 客户端--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!--lettuce 依赖 commons-pool2--> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency>spring-boot-starter-data-redis的底层客户端在 SprinBoot 2.x 版本由 Jedis 换成了 LettuceLettuce 基于 commons-pool2 实现连接池
-
配置连接信息
spring: application: name: springboot-app # redis redis: host: 127.0.0.1 port: 6379 database: 0 password: xxxx # lettuce config lettuce: pool: # 最大活跃连接数 max-active: 8 # 最大阻塞等待时间(负数表示无限制) max-wait: -1 # 池中保持的最大空闲连接数 max-idle: 8 # 至少要保持的最小空闲连接数 min-idle: 0
使用 Redis
RedisTemplate
RedisTemplate 是 SpringBoot 提供的操作 Redis 的模板对象,提供 RedisTemplate 和 StringRedisTemplate 两种类型
-
StringRedisTemplate 是 RedisTemplate 的子类,等同于 RedisTemplate<String, String>,用于操作字符串类型
@SpringBootTest public class ApplicationTests { @Autowired StringRedisTemplate stringRedisTemplate; @Test void opsForValue() { // 存 String stringRedisTemplate.opsForValue().set("name", "Cade"); // 取 String String name = stringRedisTemplate.opsForValue().get("name"); } }支持
opsForValue、opsForList、opsForSet、opsForHash等多种数据类型的操作 -
RedisTemplate<Object,Object> 是更全面的工具类,提供了操作对象的方式
RedisTemplate 默认使用 JdkSerializationRedisSerializer 进行 Object 到 String 的双向转换
可以将值的序列化修改为使用 JSON 方式,便于查看
@Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); // 设置 key 序列化方式 template.setKeySerializer(RedisSerializer.string()); // 设置 hash key 序列化方式 template.setHashKeySerializer(RedisSerializer.string()); // 设置 value 的序列化方式 template.setValueSerializer(RedisSerializer.json()); // 设置 hash value 的序列化方式 template.setHashValueSerializer(RedisSerializer.json()); return template; }RedisSerializer.json 是 Spring 提供的 Jackson 序列化方式,相当于
Jackson2JsonRedisSerializer<Object> jacksonSerializer = new Jackson2JsonRedisSerializer<>(Object.class); ObjectMapper objectMapper = new ObjectMapper(); // 将类型写入 JSON 中,JsonTypeInfo.As.PROPERTY 指写入类型的方式,如作为参数 objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY); // 老版本 Jackson 中使用 enableDefaultTyping 开启写入类型 jacksonSerializer.setObjectMapper(objectMapper); redisTemplate.setValueSerializer(jacksonSerializer);
Cache 组件
-
spring-boot-starter-cache组件提供了统一的接口和注解帮助开发者使用缓存Cache 组件提供了多种缓存的实现,如 Redis、EhCache、Caffeine 等
<!--cache 组件--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency> -
使用注解在 Redis 中缓存数据
在启动类或配置类上使用
@EnableCaching启用 Cache 组件@Cacheable // 写入缓存 @CacheEvict // 清理缓存 @CachePut // 更新缓存 @Caching // 多个注解结合使用 @CacheConfig // 写在类上表示公共的注解选项@Cacheable
key 支持 SpringEL 表达式,配合参数可以指定存到 Redis 的 key,默认使用 cacheNames:key 作为 Redis 的 key
@Cacheable(cacheNames = "string", key = "#str") public String get(String str) { // ... return str; }@CacheEvict
@CacheEvict(cacheNames = "string", key = "#str") public void remove(String str) { // ... } // 其他选项 // allEntries 是否清空所有,默认 false // beforeInvocation 是否在方法调用前清理,默认 false@CachePut
每次都会触发真实方法的调用,旨在更新对应缓存
@CachePut(cacheNames = "string", key = "#str") public String update(String str) { // ... return str; } -
配置 CacheManager
引入 Redis 和 Cache 组件后,默认配置的 RedisCacheManager 使用了 JDK Serializer 序列化 value
在 SpringBoot 1.x 可以通过传入一个 RedisTemplate 来构建 RedisCacheManager
在 SpringBoot 2.x 需要通过 RedisCacheManagerBuilder 创建 RedisCacheManager
@Bean public CacheManager redisCacheManager(RedisConnectionFactory factory) { RedisCacheConfiguration defaultConfiguration = RedisCacheConfiguration .defaultCacheConfig() .disableCachingNullValues() // 设置缓存有效期一小时 .entryTtl(Duration.ofHours(1)) // 配置 key 序列化方式 .serializeKeysWith(RedisSerializationContext.SerializationPair .fromSerializer(RedisSerializer.string())) // 配置 value 序列化方式 .serializeValuesWith(RedisSerializationContext.SerializationPair .fromSerializer(RedisSerializer.json())); return RedisCacheManager.builder(factory) // 设置默认的策略 .cacheDefaults(defaultConfiguration) // 设置指定 cache 的策略 // .withCacheConfiguration("myCacheName", defaultConfiguration) .build(); }如果配置了多个缓存管理器,注解中指定 cacheManager 可以设置要使用的缓存管理器
缓存一致性
-
缓存一致性问题
为了提高查询效率,查询数据库获取数据后,将数据写入 Redis 缓存,查询操作会先查询缓存,如果命中缓存,可以降低数据库压力,提高系统吞吐量
但是当数据需要更新,数据库和缓存的数据如何保持一致性是个问题
-
删除缓存 VS 更新缓存
对于缓存的更新,可以直接更新缓存数据,也可以删除缓存,等待下次查库后写入缓存
但是在并发情况下,直接更新缓存因为顺序无法保证,容易造成旧值覆盖新值,且增加了维护成本
如果选择加锁,则会降低系统吞吐量,这和加缓存的初衷相背
一般推荐直接让缓存失效,即删除缓存
-
更新数据库 + 删除缓存
先删除缓存,后更新数据库
线程 A:删除缓存 -------------------------------> 更新 DB 线程 B:-------- 查缓存失效 -> 查询 DB -> 写入缓存先更新数据库,后删除缓存
线程 A:--------------------- 更新 DB -> 删除缓存 线程 B:查缓存失效 -> 查询 DB -----------------> 写入缓存在并发情况下,两种方式都有一定概率会发生数据不一致问题
先更新数据库,后删除缓存的可靠性更高,因为一般查询 DB 比 更新 DB 的时间要短
-
延迟双删
延迟双删是先删除缓存再更新数据库,休眠一定时间后再次删除缓存,目的是清除读请求造成的脏缓存
具体过多久,需要根据查询数据库的耗时进行考量,一般为一秒左右
-
异步更新
通过订阅 MySQL 的 binlog 获取数据更新内容,通过 MQ 推送删除 Redis 缓存的消息
这种方式对代码的侵入性较低,也可以解决删除失败的重试问题