在高并发系统中,数据库是性能瓶颈的核心来源 ------ 频繁的查询操作会导致数据库连接耗尽、响应缓慢。Redis 作为高性能内存数据库,凭借「毫秒级响应、支持多种数据结构、分布式部署」的优势,成为缓存优化、分布式锁、计数器等场景的首选方案。合理使用 Redis 可将数据库查询压力降低 80% 以上,同时解决分布式系统中的并发竞争问题。
本文聚焦 SpringBoot 与 Redis 的实战落地,从环境搭建、基础缓存操作、分布式锁实现,到缓存三大问题(穿透、击穿、雪崩)的解决方案,全程嵌入 Java 代码教学,帮你快速掌握 Redis 核心技能,打造高可用缓存体系与并发控制方案。
一、核心认知:Redis 核心价值与适用场景
1. Redis 核心优势
- 高性能:基于内存存储,读写响应时间在 1-10 毫秒,支持每秒百万级读写操作;
- 丰富数据结构:支持 String、Hash、List、Set、Sorted Set 等,适配多场景需求;
- 高可用:支持主从复制、哨兵模式、集群部署,避免单点故障;
- 多功能:可作为缓存、分布式锁、消息队列、计数器、排行榜使用;
- 跨语言:支持多种客户端,无缝适配 Java、Python、Go 等语言。
2. 核心适用场景
- 数据缓存:热点数据(如商品详情、用户信息)缓存,减少数据库查询;
- 分布式锁:解决分布式系统并发竞争问题(如商品秒杀、订单创建);
- 会话存储:用户登录会话(如 Token)存储,替代 Session 共享;
- 实时统计:访问量计数、点赞数、排行榜(基于 Sorted Set);
- 消息队列:简单的异步通信(基于 List 实现)。
3. 缓存三大核心问题
- 缓存穿透:查询不存在的数据,缓存与数据库均无结果,导致请求直接穿透到数据库;
- 缓存击穿:热点 Key 过期瞬间,大量请求同时穿透到数据库,造成数据库压力激增;
- 缓存雪崩:大量缓存 Key 同时过期,或 Redis 服务宕机,所有请求直接打向数据库,可能导致数据库雪崩。
二、核心实战一:环境搭建(Docker + SpringBoot 集成)
1. Docker 部署 Redis
bash
运行
# 1. 拉取 Redis 镜像(6.2.7 稳定版)
docker pull redis:6.2.7
# 2. 启动 Redis 容器(配置密码,挂载数据卷持久化)
docker run -d --name redis -p 6379:6379 \
-v redis-data:/data \ # 挂载数据卷,避免重启数据丢失
-e REDIS_PASSWORD=123456 \ # 设置密码
redis:6.2.7 --requirepass 123456 # 启用密码认证
- 验证连接:使用 Redis 客户端连接
redis-cli -h localhost -p 6379 -a 123456,执行ping命令返回PONG即部署成功。
2. SpringBoot 集成 Redis 依赖与配置
(1)引入依赖(Maven)
xml
<!-- SpringBoot 集成 Redis 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Redisson 依赖(实现分布式锁,推荐) -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.23.3</version>
</dependency>
<!-- Web 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
(2)配置文件(application.yml)
yaml
spring:
redis:
host: localhost # Redis 服务地址
port: 6379 # 端口
password: 123456 # 密码
timeout: 3000ms # 连接超时时间
lettuce: # 使用 Lettuce 客户端(替代旧版 Jedis,支持集群)
pool:
max-active: 16 # 最大连接数
max-idle: 8 # 最大空闲连接数
min-idle: 4 # 最小空闲连接数
max-wait: -1ms # 最大等待时间(-1 表示无限制)
# Redisson 配置(分布式锁用)
redisson:
singleServerConfig:
address: redis://localhost:6379
password: 123456
database: 0 # 数据库索引(默认0)
三、核心实战二:基础缓存操作(注解式 + 编程式)
Spring Data Redis 提供两种缓存操作方式:注解式(简洁高效,适合简单场景)、编程式(灵活可控,适合复杂场景)。
1. 注解式缓存(@Cacheable、@CachePut、@CacheEvict)
(1)启动类开启缓存支持
java
运行
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication
@EnableCaching // 开启 Spring 缓存支持
public class RedisApplication {
public static void main(String[] args) {
SpringApplication.run(RedisApplication.class, args);
}
}
(2)Service 层使用缓存注解
java
运行
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import com.example.redis.entity.User;
import com.example.redis.mapper.UserMapper;
import javax.annotation.Resource;
@Service
public class UserService {
@Resource
private UserMapper userMapper;
// ✅ 缓存查询:key 为 "user:{id}",缓存不存在则查询数据库并存入缓存
@Cacheable(value = "user", key = "#id", unless = "#result == null")
public User getUserById(Long id) {
// 模拟数据库查询(实际开发中调用 mapper)
return userMapper.selectById(id);
}
// ✅ 缓存更新:更新数据库后同步更新缓存(key 与查询一致)
@CachePut(value = "user", key = "#user.id", unless = "#user == null")
public User updateUser(User user) {
userMapper.updateById(user);
return user;
}
// ✅ 缓存删除:删除数据库数据后清除对应缓存
@CacheEvict(value = "user", key = "#id")
public void deleteUser(Long id) {
userMapper.deleteById(id);
}
// ✅ 批量删除缓存(清除所有 user 前缀的缓存)
@CacheEvict(value = "user", allEntries = true)
public void clearUserCache() {
// 无需业务逻辑,仅清除缓存
}
}
2. 编程式缓存(RedisTemplate)
适合复杂缓存场景(如自定义缓存过期时间、多条件缓存),通过 RedisTemplate 手动操作缓存。
java
运行
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;
import com.example.redis.entity.User;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
@Service
public class UserCacheService {
@Resource
private RedisTemplate<String, Object> redisTemplate;
// 缓存前缀(避免 key 冲突)
private static final String USER_CACHE_PREFIX = "user:";
// 存入缓存(设置过期时间 30 分钟)
public void setUserCache(Long id, User user) {
String key = USER_CACHE_PREFIX + id;
ValueOperations<String, Object> valueOps = redisTemplate.opsForValue();
valueOps.set(key, user, 30, TimeUnit.MINUTES);
}
// 获取缓存
public User getUserCache(Long id) {
String key = USER_CACHE_PREFIX + id;
return (User) redisTemplate.opsForValue().get(key);
}
// 删除缓存
public void deleteUserCache(Long id) {
String key = USER_CACHE_PREFIX + id;
redisTemplate.delete(key);
}
// 自定义缓存逻辑(查询时先查缓存,无缓存则查库并存缓存)
public User getuserByIdWithCache(Long id) {
// 1. 先查缓存
User user = getUserCache(id);
if (user != null) {
return user;
}
// 2. 缓存不存在,查数据库
user = userMapper.selectById(id);
if (user != null) {
// 3. 存入缓存
setUserCache(id, user);
}
return user;
}
}
四、核心实战三:分布式锁实现(Redisson 版)
分布式系统中,多服务并发操作同一资源(如商品秒杀、库存扣减)会导致数据不一致,分布式锁可确保同一时间只有一个服务能操作资源。Redisson 封装了 Redis 分布式锁的实现,支持自动续期、公平锁、可重入锁等功能,比手动实现更稳定。
1. 分布式锁实战(商品秒杀场景)
java
运行
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;
import com.example.redis.mapper.StockMapper;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
@Service
public class SeckillService {
@Resource
private RedissonClient redissonClient;
@Resource
private StockMapper stockMapper;
// 锁前缀(避免锁 key 冲突)
private static final String STOCK_LOCK_PREFIX = "stock:lock:";
// 商品秒杀(分布式锁控制并发)
public String seckill(Long productId, Integer num) {
String lockKey = STOCK_LOCK_PREFIX + productId;
RLock lock = redissonClient.getLock(lockKey);
try {
// 1. 获取锁(最多等待 5 秒,10 秒后自动释放锁,避免死锁)
boolean isLocked = lock.tryLock(5, 10, TimeUnit.SECONDS);
if (!isLocked) {
return "秒杀火爆,请稍后再试";
}
// 2. 执行业务逻辑(查询库存 + 扣减库存)
Integer stock = stockMapper.selectStockByProductId(productId);
if (stock == null || stock < num) {
return "库存不足,秒杀失败";
}
// 扣减库存(数据库操作需加事务)
stockMapper.deductStock(productId, num);
return "秒杀成功,已扣减库存:" + num;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return "秒杀异常,请重试";
} finally {
// 3. 释放锁(仅持有锁的线程能释放)
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
2. 分布式锁核心特性(Redisson 优势)
- 可重入锁:同一线程可多次获取同一把锁,避免死锁;
- 自动续期:锁快过期时自动延长有效期,避免业务未执行完锁被释放;
- 公平锁:按请求顺序获取锁,避免饥饿问题;
- 集群支持:适配 Redis 集群模式,确保分布式环境下锁的可靠性。
五、核心实战四:缓存三大问题解决方案
1. 缓存穿透解决方案
方案一:空值缓存
查询结果为空时,也存入缓存(设置较短过期时间,如 5 分钟),避免后续请求穿透到数据库。
java
运行
public User getuserByIdWithCache(Long id) {
String key = USER_CACHE_PREFIX + id;
User user = (User) redisTemplate.opsForValue().get(key);
if (user != null) {
return user;
}
// 查数据库
user = userMapper.selectById(id);
if (user == null) {
// 空值存入缓存,设置 5 分钟过期
redisTemplate.opsForValue().set(key, new User(), 5, TimeUnit.MINUTES);
return null;
}
setUserCache(id, user);
return user;
}
方案二:布隆过滤器
提前将所有存在的 Key 存入布隆过滤器,查询时先通过过滤器判断 Key 是否存在,不存在则直接返回,避免穿透。
java
运行
// 布隆过滤器初始化(项目启动时加载所有用户 ID)
@PostConstruct
public void initBloomFilter() {
List<Long> userIdList = userMapper.selectAllUserId();
BloomFilter<Long> bloomFilter = BloomFilter.create(Funnels.longFunnel(), userIdList.size(), 0.01);
for (Long userId : userIdList) {
bloomFilter.put(userId);
}
// 存入 Redis 或本地缓存
redisTemplate.opsForValue().set("user:bloom:filter", bloomFilter);
}
// 查询时先过布隆过滤器
public User getuserByIdWithBloomFilter(Long id) {
BloomFilter<Long> bloomFilter = (BloomFilter<Long>) redisTemplate.opsForValue().get("user:bloom:filter");
if (!bloomFilter.mightContain(id)) {
return null; // 一定不存在,直接返回
}
// 后续走缓存 + 数据库逻辑
return getuserByIdWithCache(id);
}
2. 缓存击穿解决方案
方案一:热点 Key 永不过期
对热点 Key(如首页爆款商品)设置永不过期,通过后台定时任务更新缓存,避免过期瞬间穿透。
方案二:互斥锁
热点 Key 过期时,仅允许一个线程查询数据库并更新缓存,其他线程等待缓存更新完成后再获取缓存。
java
运行
public User getHotUserById(Long id) {
String key = USER_CACHE_PREFIX + id;
User user = (User) redisTemplate.opsForValue().get(key);
// 缓存过期,获取互斥锁更新缓存
if (user == null) {
RLock lock = redissonClient.getLock("user:hot:lock:" + id);
try {
if (lock.tryLock(3, 5, TimeUnit.SECONDS)) {
// 再次检查缓存(避免其他线程已更新)
user = (User) redisTemplate.opsForValue().get(key);
if (user == null) {
user = userMapper.selectById(id);
setUserCache(id, user); // 存入缓存,设置较长过期时间
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
return user;
}
3. 缓存雪崩解决方案
方案一:过期时间加随机值
给缓存 Key 设置过期时间时,添加随机值(如 0-5 分钟),避免大量 Key 同时过期。
java
运行
// 存入缓存时添加随机过期时间(30-35 分钟)
public void setUserCacheWithRandomExpire(Long id, User user) {
String key = USER_CACHE_PREFIX + id;
long expire = 30 + new Random().nextInt(5); // 随机 0-5 分钟
redisTemplate.opsForValue().set(key, user, expire, TimeUnit.MINUTES);
}
方案二:Redis 集群部署
部署 Redis 主从集群 + 哨兵模式,确保单点故障时,从节点能快速切换为主节点,避免 Redis 服务宕机导致的雪崩。
方案三:服务降级与限流
Redis 宕机时,通过限流组件(如 Sentinel)限制请求流量,或直接返回降级提示(如 "系统繁忙,请稍后再试"),保护数据库。
六、避坑指南
坑点 1:缓存注解失效,无法存入 / 获取缓存
表现:使用 @Cacheable 注解后,缓存未生效,每次都查询数据库;✅ 解决方案:确保启动类添加 @EnableCaching 注解,缓存 Key 表达式正确,方法参数与返回值可序列化(实体类实现 Serializable 接口)。
坑点 2:分布式锁死锁,资源无法释放
表现:获取锁后服务宕机,锁未释放,导致其他线程无法获取锁;✅ 解决方案:使用 Redisson 锁并设置过期时间,Redisson 会自动续期,同时确保在 finally 块中释放锁,仅持有锁的线程能释放。
坑点 3:Redis 序列化乱码,缓存数据无法解析
表现:Redis 客户端查看缓存数据为乱码,程序读取时抛出序列化异常;✅ 解决方案:自定义 RedisTemplate 序列化配置,使用 Jackson2JsonRedisSerializer 替代默认序列化器。
坑点 4:缓存与数据库数据不一致
表现:更新数据库后,缓存未同步更新,导致读取到旧数据;✅ 解决方案:更新 / 删除操作时同步更新 / 清除缓存,或使用「缓存更新策略」(如先删缓存再更数据库、延时双删)。
七、终极总结:Redis 实战的核心是「缓存合理 + 并发可控」
Redis 实战的核心并非简单的缓存存取,而是通过「合理的缓存策略」解决性能问题,通过「可靠的分布式锁」解决并发问题,同时规避缓存三大问题,确保系统高可用。企业级开发中,需结合业务场景平衡「缓存命中率」与「数据一致性」。
核心原则总结:
- 缓存策略适配业务:热点数据必缓存,低频数据不缓存,避免缓存冗余;
- 并发控制优先用成熟组件:分布式锁优先使用 Redisson,避免手动实现导致的死锁、锁超时问题;
- 提前规避缓存风险:针对穿透、击穿、雪崩问题,在设计阶段就制定解决方案,而非出现问题后补救;
- 高可用是底线:生产环境必须部署 Redis 集群,搭配主从复制与哨兵模式,避免单点故障。