在开始Spring Boot与Redis整合之前,需要确保开发环境满足以下要求:
JDK版本:推荐使用JDK 1.8或更高版本。虽然Spring Boot 2.7.x支持JDK 17,但考虑到企业级应用的稳定性,JDK 1.8仍然是更稳妥的选择。
Spring Boot版本:2.7.x系列是当前最稳定的版本,它提供了对Redis的良好支持。如果使用Spring Boot 3.x,需要注意其最低要求是JDK 17,并且部分API可能有变动。
Redis服务器:Redis 6.x及以上版本,支持多线程IO等新特性。可以在本地安装Redis,或者使用云服务提供的Redis实例。
提示:开发环境的一致性非常重要。建议使用Docker来运行Redis,这样可以避免因环境差异导致的问题。例如:
docker run --name some-redis -p 6379:6379 -d redis:6.2-alpine
对于Maven项目,需要在pom.xml中添加以下依赖:
xml复制<dependencies>
<!-- Spring Boot Starter for Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 如果需要JSON序列化 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
对于Gradle项目,build.gradle中应添加:
groovy复制dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'com.fasterxml.jackson.core:jackson-databind'
}
Spring Boot默认使用Lettuce作为Redis客户端,它基于Netty实现,支持异步和非阻塞操作,性能优于传统的Jedis。如果项目需要同步阻塞式操作,可以显式引入Jedis:
xml复制<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
在application.yml中配置Redis连接信息:
yaml复制spring:
redis:
host: 127.0.0.1
port: 6379
password: yourpassword # 如果没有密码可以省略
database: 0
timeout: 3000ms
lettuce:
pool:
max-active: 16
max-idle: 8
min-idle: 4
max-wait: 2000ms
关键参数说明:
max-active:最大连接数,根据应用并发量调整max-idle:最大空闲连接数,建议设置为max-active的50%-70%min-idle:最小空闲连接数,防止突发流量导致频繁创建连接max-wait:获取连接的最大等待时间,避免线程长时间阻塞默认的JDK序列化会导致Redis中存储的数据可读性差,且在不同JVM间可能存在兼容性问题。推荐使用JSON序列化:
java复制@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// 使用Jackson2JsonRedisSerializer来序列化和反序列化value
Jackson2JsonRedisSerializer<Object> jacksonSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.activateDefaultTyping(om.getPolymorphicTypeValidator(),
ObjectMapper.DefaultTyping.NON_FINAL);
jacksonSerializer.setObjectMapper(om);
// 使用StringRedisSerializer来序列化和反序列化key
StringRedisSerializer stringSerializer = new StringRedisSerializer();
template.setKeySerializer(stringSerializer);
template.setHashKeySerializer(stringSerializer);
template.setValueSerializer(jacksonSerializer);
template.setHashValueSerializer(jacksonSerializer);
template.afterPropertiesSet();
return template;
}
}
注意:在Spring Boot 2.7.x中,
enableDefaultTyping已被标记为废弃,推荐使用activateDefaultTyping替代。这是很多开发者升级后容易忽略的细节。
下面是一个完整的RedisService实现,封装了各种数据类型的操作:
java复制@Service
public class RedisService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// ==================== String操作 ====================
public void setString(String key, Object value, long timeout, TimeUnit unit) {
if (timeout > 0) {
redisTemplate.opsForValue().set(key, value, timeout, unit);
} else {
redisTemplate.opsForValue().set(key, value);
}
}
public Object getString(String key) {
return redisTemplate.opsForValue().get(key);
}
public Boolean delete(String key) {
return redisTemplate.delete(key);
}
// ==================== Hash操作 ====================
public void setHash(String key, String hashKey, Object value) {
redisTemplate.opsForHash().put(key, hashKey, value);
}
public Object getHash(String key, String hashKey) {
return redisTemplate.opsForHash().get(key, hashKey);
}
public Map<Object, Object> getAllHash(String key) {
return redisTemplate.opsForHash().entries(key);
}
// ==================== List操作 ====================
public void leftPush(String key, Object value) {
redisTemplate.opsForList().leftPush(key, value);
}
public Object rightPop(String key) {
return redisTemplate.opsForList().rightPop(key);
}
// ==================== Set操作 ====================
public void addToSet(String key, Object... values) {
redisTemplate.opsForSet().add(key, values);
}
public Set<Object> getSetMembers(String key) {
return redisTemplate.opsForSet().members(key);
}
// ==================== ZSet操作 ====================
public void addToZSet(String key, Object value, double score) {
redisTemplate.opsForZSet().add(key, value, score);
}
public Set<Object> getZSetRange(String key, long start, long end) {
return redisTemplate.opsForZSet().range(key, start, end);
}
}
Redis支持事务和管道操作,可以显著提升批量操作的性能:
java复制// 事务操作示例
public void transactionExample() {
redisTemplate.execute(new SessionCallback<Object>() {
@Override
public Object execute(RedisOperations operations) throws DataAccessException {
operations.multi();
operations.opsForValue().set("key1", "value1");
operations.opsForValue().set("key2", "value2");
return operations.exec();
}
});
}
// 管道操作示例
public void pipelineExample() {
redisTemplate.executePipelined(new SessionCallback<Object>() {
@Override
public Object execute(RedisOperations operations) throws DataAccessException {
for (int i = 0; i < 1000; i++) {
operations.opsForValue().set("pipeline:" + i, "value" + i);
}
return null;
}
});
}
Spring Cache抽象可以极大地简化缓存的使用:
java复制@SpringBootApplication
@EnableCaching
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
@Service
public class ProductService {
@Cacheable(value = "products", key = "#id")
public Product getProductById(Long id) {
// 模拟数据库查询
return productRepository.findById(id).orElse(null);
}
@CachePut(value = "products", key = "#product.id")
public Product updateProduct(Product product) {
return productRepository.save(product);
}
@CacheEvict(value = "products", key = "#id")
public void deleteProduct(Long id) {
productRepository.deleteById(id);
}
@Caching(evict = {
@CacheEvict(value = "productList", allEntries = true),
@CacheEvict(value = "products", key = "#id")
})
public void clearRelatedCache(Long id) {
// 清理多个相关缓存
}
}
缓存穿透解决方案:
java复制@Cacheable(value = "users", key = "#id", unless = "#result == null")
public User getUserById(Long id) {
User user = userRepository.findById(id).orElse(null);
if (user == null) {
// 缓存空值,设置较短过期时间
redisTemplate.opsForValue().set("user:null:" + id, "", 5, TimeUnit.MINUTES);
}
return user;
}
缓存击穿解决方案(使用互斥锁):
java复制public User getUserWithMutex(Long id) {
String cacheKey = "user:" + id;
User user = (User) redisTemplate.opsForValue().get(cacheKey);
if (user != null) {
return user;
}
// 获取互斥锁
String lockKey = "lock:user:" + id;
boolean locked = false;
try {
locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS);
if (locked) {
user = userRepository.findById(id).orElse(null);
if (user != null) {
redisTemplate.opsForValue().set(cacheKey, user, 1, TimeUnit.HOURS);
} else {
redisTemplate.opsForValue().set(cacheKey, null, 5, TimeUnit.MINUTES);
}
return user;
} else {
// 等待一段时间后重试
Thread.sleep(100);
return getUserWithMutex(id);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("获取用户信息中断", e);
} finally {
if (locked) {
redisTemplate.delete(lockKey);
}
}
}
Lettuce连接池推荐配置(适用于中等并发场景):
yaml复制spring:
redis:
lettuce:
pool:
max-active: 32
max-idle: 16
min-idle: 8
max-wait: 1000ms
time-between-eviction-runs: 30000ms
关键调优原则:
max-active应该大于应用的最大并发请求数min-idle应该足够应对日常流量波动max-wait不宜设置过长,避免线程堆积time-between-eviction-runs)xml复制<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
在application.yml中启用Redis指标:
yaml复制management:
endpoints:
web:
exposure:
include: health,metrics,redis
metrics:
tags:
application: ${spring.application.name}
java复制@Service
public class RedisMetricsService {
private final MeterRegistry meterRegistry;
private final RedisTemplate<String, Object> redisTemplate;
public RedisMetricsService(MeterRegistry meterRegistry,
RedisTemplate<String, Object> redisTemplate) {
this.meterRegistry = meterRegistry;
this.redisTemplate = redisTemplate;
monitorRedisLatency();
}
private void monitorRedisLatency() {
Gauge.builder("redis.command.latency", () -> {
long start = System.currentTimeMillis();
redisTemplate.opsForValue().get("healthcheck");
return System.currentTimeMillis() - start;
})
.description("Redis command latency in ms")
.register(meterRegistry);
}
}
对于生产环境,建议采用以下架构:
Spring Boot配置Redis Sentinel示例:
yaml复制spring:
redis:
sentinel:
master: mymaster
nodes: sentinel1:26379,sentinel2:26379,sentinel3:26379
password: yourpassword
code复制rename-command FLUSHDB ""
rename-command FLUSHALL ""
rename-command CONFIG ""
rename-command SHUTDOWN ""
大Key识别与处理:
java复制public void analyzeBigKeys() {
RedisConnectionFactory factory = redisTemplate.getConnectionFactory();
RedisConnection connection = factory.getConnection();
try (Cursor<byte[]> cursor = connection.scan(ScanOptions.scanOptions().count(100).build())) {
while (cursor.hasNext()) {
byte[] key = cursor.next();
Long size = connection.keyCommands().memoryUsage(key);
if (size != null && size > 1024 * 1024) { // 大于1MB视为大Key
log.warn("Big key found: {}, size: {} bytes", new String(key), size);
// 处理大Key:拆分、压缩或设置过期时间
}
}
}
}
热Key解决方案:
症状:连接超时、连接被拒绝、连接泄漏
排查步骤:
redis-cli pingtelnet redis-host 6379症状:响应慢、高延迟、吞吐量下降
优化建议:
SLOWLOG命令识别慢查询KEYS等阻塞命令常见错误:
ClassCastException:反序列化类型不匹配解决方案:
Serializable接口ObjectMapper正确处理循环引用:java复制om.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
基于Redis的RedLock算法实现分布式锁:
java复制public class RedisDistributedLock {
private final RedisTemplate<String, String> redisTemplate;
private final String lockKey;
private final String lockValue;
private final long expireTime;
public RedisDistributedLock(RedisTemplate<String, String> redisTemplate,
String lockKey, long expireTime) {
this.redisTemplate = redisTemplate;
this.lockKey = lockKey;
this.lockValue = UUID.randomUUID().toString();
this.expireTime = expireTime;
}
public boolean tryLock(long waitTime, TimeUnit unit) throws InterruptedException {
long start = System.currentTimeMillis();
long duration = unit.toMillis(waitTime);
while (true) {
Boolean acquired = redisTemplate.opsForValue()
.setIfAbsent(lockKey, lockValue, expireTime, TimeUnit.MILLISECONDS);
if (Boolean.TRUE.equals(acquired)) {
return true;
}
if (System.currentTimeMillis() - start >= duration) {
return false;
}
Thread.sleep(100); // 避免CPU空转
}
}
public void unlock() {
// 使用Lua脚本保证原子性
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else " +
"return 0 " +
"end";
redisTemplate.execute(new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(lockKey), lockValue);
}
}
利用Redis的ZSet实现延迟队列:
java复制public class RedisDelayedQueue {
private final RedisTemplate<String, Object> redisTemplate;
private final String queueKey;
public RedisDelayedQueue(RedisTemplate<String, Object> redisTemplate, String queueKey) {
this.redisTemplate = redisTemplate;
this.queueKey = queueKey;
}
public void addTask(Object task, long delay, TimeUnit unit) {
long score = System.currentTimeMillis() + unit.toMillis(delay);
redisTemplate.opsForZSet().add(queueKey, task, score);
}
public List<Object> pollTasks() {
long now = System.currentTimeMillis();
Set<Object> tasks = redisTemplate.opsForZSet().rangeByScore(queueKey, 0, now);
if (tasks != null && !tasks.isEmpty()) {
// 使用Lua脚本保证原子性
String script = "local tasks = redis.call('zrangebyscore', KEYS[1], 0, ARGV[1]) " +
"if #tasks > 0 then " +
"redis.call('zremrangebyscore', KEYS[1], 0, ARGV[1]) " +
"end " +
"return tasks";
List<Object> result = redisTemplate.execute(
new DefaultRedisScript<>(script, List.class),
Collections.singletonList(queueKey),
String.valueOf(now)
);
return result != null ? result : Collections.emptyList();
}
return Collections.emptyList();
}
}
实现热点Key自动检测与处理:
java复制@Scheduled(fixedRate = 60000) // 每分钟执行一次
public void monitorHotKeys() {
RedisConnectionFactory factory = redisTemplate.getConnectionFactory();
RedisConnection connection = factory.getConnection();
try {
// 获取最近一分钟的Key访问频率
Map<String, Long> accessCounts = new HashMap<>();
try (Cursor<byte[]> cursor = connection.scan(ScanOptions.scanOptions().count(100).build())) {
while (cursor.hasNext()) {
byte[] key = cursor.next();
Long count = connection.keyCommands().objectRefcount(key);
if (count != null && count > 1000) { // 访问次数超过阈值
accessCounts.put(new String(key), count);
}
}
}
// 处理热点Key
accessCounts.entrySet().stream()
.sorted(Map.Entry.<String, Long>comparingByValue().reversed())
.limit(10) // 处理前10个最热的Key
.forEach(entry -> {
String hotKey = entry.getKey();
long count = entry.getValue();
log.warn("Hot key detected: {}, access count: {}", hotKey, count);
// 处理策略:本地缓存、Key拆分等
if (count > 5000) {
// 将热点Key加入本地缓存
localCache.put(hotKey, redisTemplate.opsForValue().get(hotKey));
}
});
} finally {
connection.close();
}
}
主要变化:
配置调整示例(Spring Boot 3.x):
java复制@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(
RedisConnectionFactory connectionFactory,
ObjectMapper objectMapper) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
// 使用GenericJackson2JsonRedisSerializer替代Jackson2JsonRedisSerializer
GenericJackson2JsonRedisSerializer serializer =
new GenericJackson2JsonRedisSerializer(objectMapper);
template.setKeySerializer(RedisSerializer.string());
template.setHashKeySerializer(RedisSerializer.string());
template.setValueSerializer(serializer);
template.setHashValueSerializer(serializer);
return template;
}
}
java复制@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
.disableCachingNullValues()
.serializeValuesWith(SerializationPair.fromSerializer(
new GenericJackson2JsonRedisSerializer()));
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config)
.withInitialCacheConfigurations(Collections.singletonMap(
"predefined", config.entryTtl(Duration.ofHours(1))))
.transactionAware()
.build();
}
yaml复制spring:
redis:
username: default # Redis 6.x新增
password: yourpassword
yaml复制spring:
redis:
ssl: true
| 操作类型 | 单线程QPS | 10线程QPS | 管道化QPS |
|---|---|---|---|
| SET操作 | 12,000 | 45,000 | 210,000 |
| GET操作 | 15,000 | 55,000 | 230,000 |
| 事务操作 | 8,000 | 30,000 | 不适用 |
| Lua脚本 | 10,000 | 35,000 | 不适用 |
在将Spring Boot + Redis应用部署到生产环境前,请确认:
在实际项目中使用Spring Boot整合Redis时,我总结了以下经验教训:
序列化一致性:确保所有服务使用相同的序列化配置,否则会出现反序列化失败。曾经因为测试环境和生产环境序列化配置不同,导致线上事故。
连接泄漏排查:定期检查连接池状态,未正确关闭的连接会快速耗尽连接池。推荐使用@Autowired而非new来获取RedisTemplate。
缓存更新策略:先更新数据库再删除缓存,而不是直接更新缓存,避免并发写导致的数据不一致。
超时设置:Redis操作必须设置合理的超时时间,特别是生产环境网络可能不稳定。
测试覆盖:不仅要测试正常流程,还要模拟Redis不可用时的降级方案。我们的系统曾因Redis宕机导致整个服务不可用。
Key命名规范:建立统一的Key命名规范(如业务:类型:ID),避免Key冲突和混乱。
内存监控:Redis内存不足时会开始淘汰Key或拒绝写入,需要设置内存使用告警。
版本兼容性:升级Spring Boot或Redis版本前,务必在测试环境充分验证。曾经因为小版本升级导致Lettuce连接池行为变化。
本地缓存配合:对于极热的数据,可以结合Caffeine等本地缓存,减轻Redis压力。
文档完整性:所有Redis使用方式和特殊处理都要详细记录,避免成为"只有原作者知道的魔法"。