1. Spring Boot 整合 Redis 实战指南
Redis 作为当下最流行的内存数据库之一,在缓存、会话管理、排行榜等场景中发挥着重要作用。作为一名长期使用 Spring Boot 进行企业级开发的工程师,我经常需要在项目中集成 Redis。今天就来分享一下我在实际项目中的整合经验,从环境搭建到核心功能实现,带你避开那些新手容易踩的坑。
2. 环境准备与 Redis 安装
2.1 Redis 服务端安装
在开始编码前,我们需要先安装 Redis 服务端。根据不同的操作系统,安装方式略有差异:
Linux 系统(以 Ubuntu 为例)
bash复制sudo apt update
sudo apt install redis-server
sudo systemctl enable redis-server
sudo systemctl start redis-server
MacOS 系统
bash复制brew install redis
brew services start redis
Windows 系统
Windows 官方不提供 Redis 原生支持,但可以通过 WSL 或 Docker 方式运行。推荐使用 Docker 方式:
bash复制docker run --name redis -p 6379:6379 -d redis
安装完成后,可以通过以下命令验证 Redis 是否正常运行:
bash复制redis-cli ping
如果返回 "PONG",说明 Redis 服务已成功启动。
提示:生产环境建议配置密码认证和持久化策略,开发环境可以简化配置
2.2 Spring Boot 项目初始化
创建一个新的 Spring Boot 项目时,需要添加以下依赖:
xml复制<dependencies>
<!-- Spring Boot Starter for Redis -->
<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>
3. Spring Boot 配置 Redis
3.1 基础配置
在 application.properties 或 application.yml 中配置 Redis 连接信息:
properties复制# 基本配置
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.database=0
# 认证配置(如果设置了密码)
spring.redis.password=yourpassword
# 连接池配置(推荐生产环境使用)
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.min-idle=0
spring.redis.lettuce.pool.max-wait=-1ms
YAML 格式的等效配置:
yaml复制spring:
redis:
host: localhost
port: 6379
database: 0
password: yourpassword
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
max-wait: -1ms
3.2 高级配置
对于生产环境,我们通常需要更细致的配置:
java复制@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// 使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值
Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.activateDefaultTyping(mapper.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL);
serializer.setObjectMapper(mapper);
// 设置序列化方式
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(serializer);
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
}
4. Redis 核心操作实战
4.1 基本 CRUD 操作
java复制@Service
public class RedisService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 设置缓存
*/
public void set(String key, Object value) {
redisTemplate.opsForValue().set(key, value);
}
/**
* 设置缓存并指定过期时间
*/
public void setWithExpire(String key, Object value, long timeout, TimeUnit unit) {
redisTemplate.opsForValue().set(key, value, timeout, unit);
}
/**
* 获取缓存
*/
public Object get(String key) {
return redisTemplate.opsForValue().get(key);
}
/**
* 删除缓存
*/
public Boolean delete(String key) {
return redisTemplate.delete(key);
}
/**
* 判断key是否存在
*/
public Boolean hasKey(String key) {
return redisTemplate.hasKey(key);
}
}
4.2 哈希(Hash)操作
java复制public void hashOperations() {
// 设置hash值
redisTemplate.opsForHash().put("user:1001", "name", "张三");
redisTemplate.opsForHash().put("user:1001", "age", "28");
// 获取hash值
String name = (String) redisTemplate.opsForHash().get("user:1001", "name");
// 获取所有hash键值对
Map<Object, Object> entries = redisTemplate.opsForHash().entries("user:1001");
// 删除hash中的某个键
redisTemplate.opsForHash().delete("user:1001", "age");
}
4.3 列表(List)操作
java复制public void listOperations() {
// 从左侧插入
redisTemplate.opsForList().leftPush("messages", "message1");
redisTemplate.opsForList().leftPushAll("messages", "message2", "message3");
// 获取列表范围
List<Object> messages = redisTemplate.opsForList().range("messages", 0, -1);
// 从右侧弹出
Object lastMessage = redisTemplate.opsForList().rightPop("messages");
}
5. 高级特性与应用场景
5.1 发布/订阅模式
java复制@Configuration
public class RedisPubSubConfig {
@Bean
public RedisMessageListenerContainer container(RedisConnectionFactory factory,
MessageListenerAdapter listenerAdapter) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(factory);
container.addMessageListener(listenerAdapter, new PatternTopic("news.*"));
return container;
}
@Bean
public MessageListenerAdapter listenerAdapter(MessageReceiver receiver) {
return new MessageListenerAdapter(receiver, "receiveMessage");
}
}
@Component
public class MessageReceiver {
public void receiveMessage(String message, String channel) {
System.out.println("Received <" + message + "> from " + channel);
}
}
@Service
public class MessagePublisher {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public void publish(String channel, Object message) {
redisTemplate.convertAndSend(channel, message);
}
}
5.2 分布式锁实现
java复制public class RedisLockUtil {
private static final String LOCK_PREFIX = "lock:";
private static final long DEFAULT_EXPIRE_TIME = 30;
private static final TimeUnit DEFAULT_TIME_UNIT = TimeUnit.SECONDS;
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* 获取分布式锁
*/
public boolean tryLock(String lockKey, String requestId) {
return tryLock(lockKey, requestId, DEFAULT_EXPIRE_TIME, DEFAULT_TIME_UNIT);
}
public boolean tryLock(String lockKey, String requestId, long expireTime, TimeUnit timeUnit) {
String key = LOCK_PREFIX + lockKey;
return Boolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent(
key,
requestId,
expireTime,
timeUnit
));
}
/**
* 释放分布式锁
*/
public boolean releaseLock(String lockKey, String requestId) {
String key = LOCK_PREFIX + lockKey;
String value = redisTemplate.opsForValue().get(key);
if (requestId.equals(value)) {
return Boolean.TRUE.equals(redisTemplate.delete(key));
}
return false;
}
}
6. 性能优化与最佳实践
6.1 连接池配置建议
生产环境中,合理的连接池配置对性能至关重要:
properties复制# 最大连接数(根据业务量调整)
spring.redis.lettuce.pool.max-active=50
# 最大空闲连接数
spring.redis.lettuce.pool.max-idle=20
# 最小空闲连接数
spring.redis.lettuce.pool.min-idle=5
# 获取连接时的最大等待时间(毫秒)
spring.redis.lettuce.pool.max-wait=1000
6.2 序列化方案选择
根据不同的使用场景,选择合适的序列化方案:
- StringRedisSerializer:适用于简单的字符串存储
- Jackson2JsonRedisSerializer:适用于复杂对象存储,支持JSON格式
- JdkSerializationRedisSerializer:Java原生序列化,兼容性好但效率较低
6.3 批量操作优化
对于大批量数据操作,使用管道(Pipeline)可以显著提高性能:
java复制public void pipelineExample() {
redisTemplate.executePipelined(new RedisCallback<Object>() {
@Override
public Object doInRedis(RedisConnection connection) throws DataAccessException {
for (int i = 0; i < 1000; i++) {
connection.set(("key:" + i).getBytes(), ("value:" + i).getBytes());
}
return null;
}
});
}
7. 常见问题与解决方案
7.1 连接超时问题
问题现象:连接Redis时出现超时错误
解决方案:
- 检查Redis服务是否正常运行
- 检查防火墙设置,确保端口开放
- 适当增加连接超时时间配置:
properties复制spring.redis.timeout=3000
7.2 序列化异常
问题现象:存储对象后读取时出现反序列化错误
解决方案:
- 确保使用一致的序列化方式
- 检查对象是否实现了Serializable接口
- 推荐使用Jackson2JsonRedisSerializer
7.3 内存溢出
问题现象:Redis内存占用过高
解决方案:
- 设置合理的过期时间
- 定期清理无用key
- 配置内存淘汰策略:
properties复制# 在Redis配置文件中设置 maxmemory-policy allkeys-lru
8. 实际项目中的应用案例
8.1 缓存穿透防护
java复制public Object getWithCachePenetrationProtection(String key, Class<T> type, long expire, TimeUnit unit,
Supplier<T> loader) {
// 1. 从缓存获取
Object value = redisTemplate.opsForValue().get(key);
if (value != null) {
return value;
}
// 2. 获取分布式锁
String lockKey = "lock:" + key;
boolean locked = lockUtil.tryLock(lockKey, "requestId", 10, TimeUnit.SECONDS);
if (!locked) {
// 获取锁失败,稍后重试
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return getWithCachePenetrationProtection(key, type, expire, unit, loader);
}
try {
// 双重检查
value = redisTemplate.opsForValue().get(key);
if (value != null) {
return value;
}
// 3. 从数据源加载
value = loader.get();
// 4. 空值也缓存,防止缓存穿透
if (value == null) {
redisTemplate.opsForValue().set(key, "", 5, TimeUnit.MINUTES);
} else {
redisTemplate.opsForValue().set(key, value, expire, unit);
}
return value;
} finally {
// 释放锁
lockUtil.releaseLock(lockKey, "requestId");
}
}
8.2 热点数据缓存
java复制@Cacheable(value = "products", key = "#id", unless = "#result == null")
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);
}
在实际项目中集成 Redis 时,我发现合理设计键命名规范非常重要。我通常采用 "业务名:对象名:ID" 的格式,如 "order:detail:123"。这样既清晰又便于管理,还能利用 Redis 的模式匹配功能进行批量操作。