摘要
- 本文介绍 SpringBoot 集成 Redis 的方法
- 本文基于
redis-7.4.7,springboot-3.5.8
- Redis官网:https://redis.io/
常见面试题
引入依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>3.5.8</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency> </dependencies>
|
配置 application.yml
单机模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| spring: data: redis: host: 127.0.0.1 port: 6379 database: 0 username: admin password: redis123 timeout: 5s connectTimeout: 3s clientName: demo-service lettuce: shutdown-timeout: 100ms pool: max-active: 64 max-idle: 32 min-idle: 16 max-wait: 2s time-between-eviction-runs: 30s
|
| 配置项 |
示例值 |
含义 |
默认值 |
生产建议 |
spring.data.redis.host |
localhost |
Redis 服务地址(IP / 域名) |
localhost |
生产使用内网 IP / 域名 |
spring.data.redis.port |
6379 |
Redis 服务端口 |
6379 |
一般无需修改 |
spring.data.redis.username |
admin |
Redis ACL 用户名(Redis 6+) |
(空) |
未启用 ACL 可不配置 |
spring.data.redis.password |
123456 |
Redis 访问密码 |
(空) |
生产必须配置 |
spring.data.redis.database |
0 |
逻辑数据库索引(0–15) |
0 |
Cluster 模式无效 |
spring.data.redis.timeout |
3s |
Redis 命令执行超时 |
60s(依版本) |
建议 2–5s |
spring.data.redis.connect-timeout |
3s |
TCP 建连超时 |
OS 默认 |
建议 1–5s |
spring.data.redis.client-name |
my-redis-client |
客户端标识,用于运维定位 |
(空) |
强烈建议配置 |
| 配置项 |
示例值 |
含义 |
默认值 |
生产建议 |
spring.data.redis.lettuce.pool.max-active |
8 |
连接池最大连接数(使用中 + 空闲) |
8 |
CPU × 2~4 或压测评估 |
spring.data.redis.lettuce.pool.max-wait |
2s |
连接耗尽时等待时间 |
-1(无限等待) |
必须设置,1–3s |
spring.data.redis.lettuce.pool.max-idle |
8 |
最大空闲连接数 |
8 |
max-active × 30%~50% |
spring.data.redis.lettuce.pool.min-idle |
0 |
最小空闲连接数 |
0 |
≥ max-active × 25% |
spring.data.redis.lettuce.pool.time-between-eviction-runs |
60s |
空闲连接检测周期 |
-1(不启用) |
30–60s |
| 配置项 |
示例值 |
含义 |
默认值 |
生产建议 |
spring.data.redis.lettuce.shutdown-timeout |
100ms |
应用关闭时等待连接释放时间 |
100ms |
一般无需修改 |
sentinel 模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| spring: data: redis: database: 0 username: admin password: redis123 timeout: 5s connectTimeout: 3s clientName: demo-service sentinel: master: mymaster nodes: - 10.0.0.10:26379 - 10.0.0.11:26379 - 10.0.0.12:26379 lettuce: shutdown-timeout: 100ms pool: max-active: 64 max-idle: 32 min-idle: 16 max-wait: 2s time-between-eviction-runs: 30s
|
| 配置项 |
示例值 |
含义 |
是否必填 |
说明 |
spring.data.redis.sentinel.master |
mymaster |
Sentinel 监控的 Master 名称 |
✅ |
必须与 Sentinel monitor 名称完全一致 |
spring.data.redis.sentinel.nodes |
10.0.0.10:26379,10.0.0.11:26379,10.0.0.12:26379 |
Sentinel 节点列表 |
✅ |
至少配置 2–3 个 Sentinel,提升可用性 |
spring.data.redis.sentinel.username |
(空) |
Sentinel 的认证用户名 |
|
如果启用 ACL,则必填 |
spring.data.redis.sentinel.password |
(空) |
Sentinel 的认证密码 |
|
如果启用 ACL,则必填 |
cluster 模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| spring: data: redis: username: admin password: redis123 timeout: 5s connectTimeout: 3s clientName: order-service cluster: nodes: - 192.168.1.10:6379 - 192.168.1.11:6379 - 192.168.1.12:6379 lettuce: shutdown-timeout: 100ms cluster: refresh: adaptive: true period: 10s pool: max-active: 64 max-idle: 32 min-idle: 16 max-wait: 2s time-between-eviction-runs: 30s
|
1
| 集群模式下,不能配置 `spring.data.redis.database`,因为集群只能使用默认数据库索引 0
|
| 配置项 |
示例值 |
含义 |
是否必填 |
默认值 |
生产建议 |
spring.data.redis.cluster.nodes |
192.168.1.10:6379,... |
Redis Cluster 节点地址列表 |
✅ |
(空) |
至少配置 3 个节点 |
| 配置项 |
示例值 |
含义 |
默认值 |
是否推荐 |
adaptive |
true |
开启 事件驱动拓扑刷新 |
false |
✅ 必须 |
period |
10s |
开启 定时拓扑刷新周期 |
关闭 |
✅ 必须 |
SpringBoot 配置类
封装 RedisTemplate
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| package com.example.config;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration public class RedisConfig {
@Bean public ObjectMapper redisObjectMapper() { return JsonMapper.builder() .addModule(new JavaTimeModule()) .serializationInclusion(JsonInclude.Include.NON_NULL) .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) .build(); }
@Bean public RedisTemplate<String, Object> redisTemplate( RedisConnectionFactory connectionFactory, ObjectMapper objectMapper) {
RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(connectionFactory);
StringRedisSerializer stringSerializer = new StringRedisSerializer();
GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer(objectMapper);
template.setKeySerializer(stringSerializer); template.setHashKeySerializer(stringSerializer);
template.setValueSerializer(jsonSerializer); template.setHashValueSerializer(jsonSerializer);
template.afterPropertiesSet(); return template; } }
|
开启注解式缓存
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
| package com.example.config;
import lombok.Getter; import lombok.Setter; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.cache.CacheManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.cache.RedisCacheWriter; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.RedisSerializationContext;
import java.time.Duration; import java.util.HashMap; import java.util.Map;
@Configuration @AutoConfigureAfter(value = RedisConfig.class)
@ConfigurationProperties(prefix = "caching") public class RedisCachingConfig {
@Getter @Setter private Map<String, Long> ttlmap;
@Bean public CacheManager cacheManager(RedisTemplate<String, Object> redisTemplate) { return RedisCacheManager .builder(RedisCacheWriter.nonLockingRedisCacheWriter(redisTemplate.getConnectionFactory())) .cacheDefaults(redisCacheConfiguration(redisTemplate, 3600L)) .withInitialCacheConfigurations(initialRedisCacheConfiguration(redisTemplate)) .build(); }
private RedisCacheConfiguration redisCacheConfiguration(RedisTemplate<String, Object> redisTemplate, Long ttl) {
return RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofSeconds(ttl)) .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisTemplate.getStringSerializer())) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(redisTemplate.getValueSerializer())); }
private Map<String, RedisCacheConfiguration> initialRedisCacheConfiguration(RedisTemplate<String, Object> redisTemplate) { Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap<>(); for (Map.Entry<String, Long> entry : ttlmap.entrySet()) { redisCacheConfigurationMap.put(entry.getKey(), redisCacheConfiguration(redisTemplate, entry.getValue())); } return redisCacheConfigurationMap; }
}
|
1 2 3 4
| caching: ttlmap: commonCache: 3600 loginCache: 7200
|
1 2 3 4 5 6 7 8
| @EnableCaching @SpringBootApplication public class DemoApplication {
public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| @Service
@CacheConfig(cacheNames = "commonCache") public class SystemUserServiceImpl implements ISystemUserService { @Autowired SystemUserJpaRepository systemUserJpaRepository;
@Cacheable(key = "'SystemUserServiceImpl.findAll'") public List<SystemUser> findAll() { List<SystemUser> systemUserList = systemUserJpaRepository.findAll(); return systemUserList; } @Cacheable(key = "'SystemUserServiceImpl.findById_'+ #userId") public SystemUser findById(String userId) { return systemUserJpaRepository.findById(userId); } @CacheEvict(key = "'SystemUserServiceImpl.findById_'+ #userId") public void deleteById(String userId) { return systemUserJpaRepository.deleteById(userId); } @CacheEvict(allEntries = true, beforeInvocation = true) public SystemUser add(SystemUser user) { systemUserJpaRepository.save(user); return user; } }
|
如何保障 MySql 和 Redis 数据一致性?
先把结论说死:MySQL 和 Redis 是两个独立系统,没有分布式事务,就不存在真正的强一致。 能做到的是最终一致 + 有兜底。所以方案的目标不是「永不出错」,而是不一致的窗口足够短、出错能自愈。真要强一致,就别用缓存,或者读写都加分布式锁(见 Redisson 简介)。
四种写法,为什么只推荐一种
以「更新一条数据」为例,缓存和 DB 的操作顺序一共四种组合:
| 写法 |
问题 |
结论 |
| 先更新 DB,再更新缓存 |
并发写会乱序:A 改 DB → B 改 DB → B 写缓存 → A 写缓存,缓存最终是 A 的旧值 |
不用 |
| 先删缓存,再更新 DB |
删完到 DB 提交之间,读请求会把旧值重新加载进缓存,之后长期是脏数据 |
不用 |
| 先更新 DB,再删缓存 |
仍有极小窗口(读请求恰好在 DB 提交前读到旧值、在删缓存后才回填),但要求读比写还慢,概率很低 |
推荐,即 Cache-Aside |
| 先删缓存 → 更新 DB → 延迟再删一次 |
用第二次删兜住上面的窗口,代价是多一次删除和延迟逻辑 |
高要求时叠加 |
Spring Cache 的 @CacheEvict 就是第三种语义。注意上文示例里的 beforeInvocation = true 表示方法执行前就删缓存,等于退化成「先删缓存再更新 DB」;对一致性敏感的写操作应该用默认的 beforeInvocation = false(方法成功返回后才删),失败就不删。
为什么是「删缓存」而不是「更新缓存」
-
删除是幂等的,重复执行、乱序执行结果都一样,天生抗并发;写缓存则要拼顺序。
-
懒加载更省:写多读少时,更新缓存的计算大多会被下一次更新覆盖,白算。
-
成本低:删只需要 key,更新要能拼出完整的缓存值(宽对象往往还得回查几张表)。
真正要解决的是「删缓存这一步可能失败」
DB 提交成功、删缓存失败,缓存就一直是脏的。三种兜法,从简单到干净:
-
重试:删失败进本地重试队列或 MQ 延迟重试,不要在业务线程里死等。
-
延迟双删:更新 DB 后立刻删一次,再延迟 N 毫秒删第二次,用来清掉窗口期被回填的旧值。延迟要大于一次读操作(含回填)的耗时,一般几百毫秒到 1 秒,靠压测定。
-
订阅 binlog 异步删(推荐):业务代码只管写 MySQL,用 Canal/Debezium 监听 binlog 再删对应的 key。好处是业务无侵入、不会漏(位点可重放),和 MySQL 同步到 ES 是同一条链路和同一套保证,见 MySql--binlog。
两个容易漏掉的坑
-
主从延迟:读写分离时,更新主库并删缓存后,读请求打到从库拿到旧数据又写回缓存,于是缓存又脏了。要么延迟删的时间覆盖主从延迟,要么这类查询强制读主库。半同步复制见 MySql单节点、主从、双主的构建方法。
-
TTL 是最后一道防线,必须设。上文 entryTtl 那段就是干这个的:所有删除逻辑都失效时,缓存也会在过期后自动回到正确值。不设 TTL 的缓存出一次问题就是永久脏数据。不同分组设不同 TTL 还能顺带打散过期时间,避免缓存雪崩。
按业务要求选
-
能容忍秒级不一致(大多数读多写少的查询):Cache-Aside + TTL,就够了。
-
不想让业务代码管缓存、或删除不能丢:Cache-Aside + binlog 异步删。
-
短时间内绝不能读到旧值(余额、库存):不要用缓存兜,直接读 MySQL,或用 Redisson 分布式锁把读写串起来,见 一个基于 Redis 的可重入分布式锁的实现。
一句话总结:先写 MySQL,再删 Redis;删除要保证不丢(重试或 binlog 订阅);所有 key 都设 TTL 兜底。 不要更新缓存、不要先删后写,也不要指望缓存和 DB 强一致。