SpringBoot 集成 Redis

摘要

  • 本文介绍 SpringBoot 集成 Redis 的方法
  • 本文基于redis-7.4.7springboot-3.5.8
  • Redis官网:https://redis.io/

常见面试题

引入依赖

  • pom.xml中引入依赖

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>
<!-- redis 连接池 -->
<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 客户端标识,用于运维定位 (空) 强烈建议配置
  • 连接池配置(Lettuce Pool)

配置项 示例值 含义 默认值 生产建议
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
  • Lettuce 客户端运行参数

配置项 示例值 含义 默认值 生产建议
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 个节点
  • Cluster 拓扑自动刷新配置,用于解决集群拓扑变化时 Client 无法自动感知

配置项 示例值 含义 默认值 是否推荐
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);

// --- Key 序列化 ---
StringRedisSerializer stringSerializer = new StringRedisSerializer();

// --- Value 序列化 ---
// 使用 Jackson JSON,避免 JDK 序列化性能与安全问题
GenericJackson2JsonRedisSerializer jsonSerializer =
new GenericJackson2JsonRedisSerializer(objectMapper);

// Key
template.setKeySerializer(stringSerializer);
template.setHashKeySerializer(stringSerializer);

// Value
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)
//注入redis分组配置属性:ttlmap
@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)) //设置过期,单位秒
//.disableCachingNullValues() //不允许存储null值,默认可以存储null,缓存null可以防止"缓存穿透"
//.disableKeyPrefix() //设置key前面不带前缀,最好不要去掉前缀,否则执行删除缓存时会清空全部缓存
.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;
}

}
  • ttlmap 配置项

1
2
3
4
caching:
ttlmap:
commonCache: 3600
loginCache: 7200
  • 启动类上要加 @EnableCaching

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 强一致。