引言:入职3个月,被"缓存乱象"逼到怀疑自己
搞定Java基础、Spring Boot,能熟练用MySQL实现业务存储后,我一度觉得自己已经能应对大部分后端开发场景。可当项目用户量上涨,接口响应越来越慢,甚至出现MySQL连接超时的问题时,我彻底慌了------原来项目里没有任何缓存机制,所有请求都直接穿透到数据库,哪怕是高频访问的首页数据、字典数据,也得每次都去数据库查询一遍。
为了缓解数据库压力,我尝试用Java的HashMap做本地缓存,可问题接踵而至:本地缓存无法跨服务共享,分布式部署时各节点缓存不一致,修改数据后缓存无法及时更新,导致用户看到脏数据;更麻烦的是,HashMap没有过期机制,缓存数据越积越多,占用大量内存,最后直接导致服务OOM。
有一次,因为本地缓存没有及时清理,用户修改了个人信息后,刷新页面还是显示旧数据,投诉电话都打到了领导那里。旁边的李哥看我焦头烂额,拍了拍我的肩膀说:"小王,本地缓存只适合单机简单场景,面对分布式、高并发,Redis才是后端开发的'缓存神器'------它不仅能实现分布式缓存共享,还支持过期策略、多种数据结构,既能缓解数据库压力,又能解决缓存一致性问题,兼顾性能与可靠性。"
那天之后,我彻底投入Redis的学习,从最基础的安装配置、核心数据结构,到Spring Boot整合Redis、缓存策略、分布式场景应用,再到性能优化、故障排查,一步步摆脱了"缓存乱象"的困扰,慢慢学会了用Redis为项目高效赋能。今天,就把这段从"被缓存折磨"到"掌控缓存"的成长历程,分享给和曾经的我一样,被缓存问题困扰、想提升系统性能的Java新手开发者。
注:本文聚焦Redis 7.x(当前主流稳定版本),结合Spring Boot整合Redis的实战场景,不冗余讲解过时用法,全程用真实业务场景串联知识点,从入门配置到高级技巧,从新手踩坑到规范落地,融入趣味类比,避开枯燥说教,让你看完就能上手,真正把Redis用在日常开发中,实现"缓存高效管控、系统性能翻倍"。
第一章:入门破局------吃透Redis基础,告别缓存乱象
李哥告诉我:"Redis入门很简单,核心就是'基于内存的键值对数据库',相当于你把高频访问的数据存到内存里,查询时直接从内存读取,比从磁盘数据库查询快上百倍。新手不用一开始就追求复杂用法,先把基础的安装、核心数据结构、Spring Boot整合搞懂,就能解决大部分缓存场景的需求。"
对于Java新手来说,Redis的进阶第一步,就是吃透以下基础知识点,快速摆脱缓存乱象的困扰。
一、先搞懂:Redis到底是什么?(趣味类比,一看就懂)
很多新手一听到Redis,就会和MySQL混淆,甚至觉得"有了MySQL,就不用学Redis了",其实这是一个很大的误区。一句话讲明白Redis的定位:Redis是一个"基于内存的高性能键值对数据库",它不替代MySQL,而是作为"缓存"辅助MySQL,核心作用是"减轻数据库压力、提升接口响应速度"。
举个通俗的例子:如果把数据库(MySQL)比作"仓库",里面存放着所有的货物(数据),每次取货(查询数据)都要走进仓库慢慢找,速度很慢;而Redis就像"仓库门口的货架",我们把高频取货的货物(高频访问数据)放在货架上,取货时直接从货架上拿,不用进仓库,速度大幅提升。
补充:Redis的核心优势有3点------① 速度快:基于内存操作,响应时间毫秒级;② 支持多种数据结构:不止字符串,还有哈希、列表、集合等,能满足不同业务场景;③ 支持分布式:可集群部署,实现缓存共享,解决分布式系统的缓存一致性问题。这也是为什么几乎所有高并发项目,都会用Redis作为缓存中间件。
二、环境搭建(Spring Boot整合Redis,最常用场景)
新手最容易踩的坑:手动导入Redis依赖版本不兼容、配置参数错误,导致项目启动报错或无法连接Redis。其实Spring Boot整合Redis非常简单,只需3步:导入依赖、配置参数、编写工具类,新手可直接复制粘贴。
1. Maven依赖(pom.xml中添加)
Spring Boot提供了Redis的starter依赖,自动整合Redis和Spring,无需手动配置版本,适配Spring Boot所有版本:
xml
<!-- Spring Boot 3.2.x整合Redis核心依赖 -->
<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>
<!-- 可选:lombok简化代码 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
2. 配置文件(application.yml)
配置Redis连接信息、连接池参数、序列化方式(避免存储乱码),新手直接修改自己的Redis地址、密码即可:
yaml
spring:
# Redis基础配置
redis:
host: 127.0.0.1 # Redis服务器地址
port: 6379 # Redis端口
password: 123456 # Redis密码(无则省略)
database: 0 # 数据库索引(0-15)
timeout: 10000 # 连接超时时间(毫秒)
# Lettuce连接池配置
lettuce:
pool:
max-active: 8 # 最大连接数
max-idle: 8 # 最大空闲连接数
min-idle: 2 # 最小空闲连接数
max-wait: 10000 # 最大等待时间(毫秒)
# MyBatis配置(保持原有结构)
mybatis:
configuration:
map-underscore-to-camel-case: true
mapper-locations: classpath:mapper/**/*.xml
type-aliases-package: com.example.demo.entity
3. 关键配置说明(新手必看)
-
host/port/password:Redis的连接信息,本地测试时host为127.0.0.1,端口默认6379,若Redis未设置密码,可删除password配置;
-
lettuce pool:Redis连接池配置,Spring Boot 3.2.x默认使用Lettuce连接池(替代之前的Jedis),性能更优,max-active、max-idle等参数可根据业务并发量调整;
-
序列化配置:Redis默认使用JDK序列化,存储对象时会出现乱码,推荐使用Jackson序列化,后续会通过配置类实现,确保存储的数据可读;
-
database:Redis默认有16个数据库,可通过该配置切换,不同数据库之间数据隔离,适合多模块项目分开存储缓存。
4. 编写Redis配置类(实现序列化,避免乱码)
Spring Boot整合Redis后,默认的序列化方式会导致对象存储乱码,因此需要编写配置类,自定义序列化规则,新手可直接复制使用:
java
package com.example.demo.config;
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;
/**
* Redis配置类:解决序列化乱码问题
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
// 1. 创建RedisTemplate对象
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory);
// 2. 配置序列化方式
StringRedisSerializer stringSerializer = new StringRedisSerializer();
GenericJackson2JsonRedisSerializer jacksonSerializer = new GenericJackson2JsonRedisSerializer();
// Key和HashKey使用String序列化
redisTemplate.setKeySerializer(stringSerializer);
redisTemplate.setHashKeySerializer(stringSerializer);
// Value和HashValue使用Jackson序列化
redisTemplate.setValueSerializer(jacksonSerializer);
redisTemplate.setHashValueSerializer(jacksonSerializer);
// 3. 初始化RedisTemplate
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
三、核心:Redis核心数据结构(Redis的"灵魂",必掌握)
Redis的强大之处,在于它支持多种数据结构,不同数据结构对应不同的业务场景,新手重点掌握以下5种核心数据结构,就能应对90%的日常缓存场景。李哥告诉我:"吃透Redis数据结构,就等于掌握了Redis的一半,每种结构都有其独特的用法,不要只会用字符串存储。"
1. 字符串(String)------最基础、最常用
核心说明
字符串是Redis最基础的数据结构,value可以是字符串、数字(整数/浮点数),最大容量为512MB,适合存储简单的键值对数据(如用户Token、验证码、计数器等)。
常用命令(实战高频)
-
SET key value:设置键值对(覆盖已有key);
-
GET key:获取指定key的value;
-
SETEX key seconds value:设置键值对,并指定过期时间(秒);
-
INCR key:将key对应的数字值自增1(适合计数器,如文章阅读量);
-
DECR key:将key对应的数字值自减1;
-
DEL key:删除指定key。
实战场景
存储用户登录Token、手机验证码(设置5分钟过期)、文章阅读量计数。
Spring Boot代码示例
java
// 注入RedisTemplate
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// 1. 存储用户Token(设置2小时过期)
String token = "user:token:1001"; // key命名规范:模块:功能:唯一标识
redisTemplate.opsForValue().set(token, "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", 2, TimeUnit.HOURS);
// 2. 获取用户Token
String userToken = (String) redisTemplate.opsForValue().get(token);
// 3. 文章阅读量自增(key:article:readCount:1001)
redisTemplate.opsForValue().increment("article:readCount:1001");
2. 哈希(Hash)------适合存储对象
核心说明
哈希结构是"键值对中的键值对",即一个key对应一个hash表,hash表中包含多个field-value对,适合存储对象(如用户信息、商品信息),可以单独修改对象的某个字段,无需修改整个对象,节省内存。
常用命令(实战高频)
-
HSET key field value:给hash表的指定field设置value;
-
HGET key field:获取hash表中指定field的value;
-
HGETALL key:获取hash表中所有的field-value对;
-
HDEL key field:删除hash表中指定的field;
-
HEXISTS key field:判断hash表中是否存在指定field。
实战场景
存储用户信息(key:user:info:1001,field:username、phone、balance)、商品信息,支持单独修改用户余额、商品价格。
Spring Boot代码示例
java
// 1. 存储用户信息(hash结构)
String userKey = "user:info:1001";
redisTemplate.opsForHash().put(userKey, "username", "小王");
redisTemplate.opsForHash().put(userKey, "phone", "13800138000");
redisTemplate.opsForHash().put(userKey, "balance", 1000.0);
// 2. 获取用户的手机号
String phone = (String) redisTemplate.opsForHash().get(userKey, "phone");
// 3. 修改用户余额(单独修改field,无需修改整个对象)
redisTemplate.opsForHash().put(userKey, "balance", 1500.0);
// 4. 获取用户所有信息
Map<Object, Object> userInfo = redisTemplate.opsForHash().entries(userKey);
3. 列表(List)------适合有序集合场景
核心说明
列表是有序、可重复的元素集合,底层是双向链表,支持从头部或尾部添加、删除元素,适合存储有序数据(如消息队列、最新公告列表、分页列表)。
常用命令(实战高频)
-
LPUSH key value1 value2...:从列表头部添加一个或多个元素;
-
RPUSH key value1 value2...:从列表尾部添加一个或多个元素;
-
LPOP key:从列表头部删除并返回一个元素;
-
RPOP key:从列表尾部删除并返回一个元素;
-
LRANGE key start end:获取列表中从start到end的元素(start=0,end=-1表示所有元素)。
实战场景
简单消息队列(如用户通知、订单消息)、最新公告列表(按时间顺序存储,取最新5条)。
Spring Boot代码示例
java
// 1. 往公告列表尾部添加3条公告(key:notice:list)
String noticeKey = "notice:list";
redisTemplate.opsForList().rightPush(noticeKey, "公告1:Redis实战指南上线");
redisTemplate.opsForList().rightPush(noticeKey, "公告2:Spring Boot 3.2.x更新");
redisTemplate.opsForList().rightPush(noticeKey, "公告3:系统维护通知");
// 2. 获取最新2条公告(从尾部取2条)
List<Object> latestNotices = redisTemplate.opsForList().range(noticeKey, -2, -1);
// 3. 从头部删除一条公告(模拟消息消费)
Object notice = redisTemplate.opsForList().leftPop(noticeKey);
4. 集合(Set)------适合无重复、无序场景
核心说明
集合是无序、不可重复的元素集合,支持交集、并集、差集操作,适合存储无重复的集合数据(如用户标签、商品分类、好友列表)。
常用命令(实战高频)
-
SADD key member1 member2...:向集合中添加一个或多个元素(重复元素会自动去重);
-
SMEMBERS key:获取集合中所有元素;
-
SISMEMBER key member:判断集合中是否存在指定元素;
-
SREM key member1 member2...:从集合中删除一个或多个元素;
-
SINTER key1 key2:获取两个集合的交集(如两个用户的共同好友);
-
SUNION key1 key2:获取两个集合的并集;
-
SDIFF key1 key2:获取两个集合的差集(key1中有、key2中没有的元素)。
实战场景
用户标签(一个用户可多个标签,标签不重复)、商品分类、共同好友查询。
Spring Boot代码示例
java
// 1. 给用户1001添加标签(key:user:tag:1001)
String tagKey = "user:tag:1001";
redisTemplate.opsForSet().add(tagKey, "Java", "Redis", "Spring Boot");
// 2. 判断用户是否有Redis标签
boolean hasRedisTag = redisTemplate.opsForSet().isMember(tagKey, "Redis");
// 3. 查询用户1001和用户1002的共同标签(用户1002的标签key:user:tag:1002)
Set<Object> commonTags = redisTemplate.opsForSet().intersect(tagKey, "user:tag:1002");
5. 有序集合(Sorted Set/ZSet)------适合排序、排名场景
核心说明
有序集合是"有序、不可重复"的元素集合,每个元素都有一个"分数(score)",Redis根据分数对元素进行排序(默认升序),适合需要排序、排名的场景(如排行榜、积分排名)。
常用命令(实战高频)
-
ZADD key score1 member1 score2 member2...:向有序集合中添加元素,并指定分数;
-
ZRANGE key start end [WITHSCORES]:获取有序集合中从start到end的元素(升序),加上WITHSCORES可显示分数;
-
ZREVRANGE key start end [WITHSCORES]:获取有序集合中从start到end的元素(降序);
-
ZINCRBY key increment member:给有序集合中指定元素的分数增加increment;
-
ZRANK key member:获取指定元素在有序集合中的排名(升序,从0开始);
-
ZREVRANK key member:获取指定元素在有序集合中的排名(降序,从0开始)。
实战场景
商品销量排行榜、用户积分排名、文章点赞排行榜。
Spring Boot代码示例
java
// 1. 存储商品销量(key:goods:sales:rank,score:销量,member:商品ID)
String salesRankKey = "goods:sales:rank";
redisTemplate.opsForZSet().add(salesRankKey, "goods_1001", 100);
redisTemplate.opsForZSet().add(salesRankKey, "goods_1002", 200);
redisTemplate.opsForZSet().add(salesRankKey, "goods_1003", 150);
// 2. 给商品1001增加50销量
redisTemplate.opsForZSet().incrementScore(salesRankKey, "goods_1001", 50);
// 3. 获取销量前2名的商品(降序,显示分数)
Set<ZSetOperations.TypedTuple<Object>> top2Sales = redisTemplate.opsForZSet().reverseRangeWithScores(salesRankKey, 0, 1);
// 4. 获取商品1002的销量排名(降序)
Long rank = redisTemplate.opsForZSet().reverseRank(salesRankKey, "goods_1002");
6. 趣味小总结
以前用HashMap做本地缓存,只能存储简单的键值对,还会出现乱码、缓存不一致的问题;现在有了Redis,5种核心数据结构按需使用:字符串存Token、哈希存对象、列表存消息、集合存标签、有序集合做排名,既能满足不同业务场景,又能解决缓存乱象,让缓存管理变得优雅又高效。
四、入门实战:Service层调用Redis,实现缓存功能
Redis配置完成、核心数据结构掌握后,就可以在Service层注入RedisTemplate,调用其方法,实现缓存的增删改查,结合MySQL实现"缓存+数据库"的双重存储,新手可直接复制测试:
1. 场景准备
沿用之前的User实体类(用户信息),实现"查询用户信息时,先查缓存,缓存没有再查数据库,查询后存入缓存"的逻辑,缓解数据库压力。
2. Service层代码示例
java
package com.example.demo.service;
import com.example.demo.entity.User;
import com.example.demo.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class UserService {
// 注入UserMapper(操作数据库)
private final UserMapper userMapper;
// 注入RedisTemplate(操作Redis)
private final RedisTemplate<String, Object> redisTemplate;
// 构造方法注入(Spring Boot 3.2.x 可省略@Autowired)
@Autowired
public UserService(UserMapper userMapper, RedisTemplate<String, Object> redisTemplate) {
this.userMapper = userMapper;
this.redisTemplate = redisTemplate;
}
// 缓存key前缀(规范命名,避免key冲突)
private static final String USER_CACHE_KEY = "user:info:";
/**
* 根据ID查询用户(缓存优先)
*/
public User getUserById(Long id) {
// 1. 构建缓存key
String cacheKey = USER_CACHE_KEY + id;
// 2. 先查缓存
User user = (User) redisTemplate.opsForValue().get(cacheKey);
if (user != null) {
// 缓存存在,直接返回
return user;
}
// 3. 缓存不存在,查数据库
user = userMapper.getUserById(id);
if (user != null) {
// 4. 查询结果存入缓存,设置1小时过期(避免缓存过期)
redisTemplate.opsForValue().set(cacheKey, user, 1, TimeUnit.HOURS);
}
return user;
}
/**
* 修改用户信息(同步更新缓存,避免脏数据)
*/
public int updateUser(User user) {
// 1. 修改数据库
int rows = userMapper.updateUser(user);
if (rows > 0) {
// 2. 修改成功,更新缓存(删除旧缓存,下次查询自动重建)
String cacheKey = USER_CACHE_KEY + user.getId();
redisTemplate.delete(cacheKey);
}
return rows;
}
/**
* 根据ID删除用户(同步删除缓存)
*/
public int deleteUserById(Long id) {
// 1. 删除数据库数据
int rows = userMapper.deleteUserById(id);
if (rows > 0) {
// 2. 删除缓存,避免脏数据
String cacheKey = USER_CACHE_KEY + id;
redisTemplate.delete(cacheKey);
}
return rows;
}
}
3. 核心逻辑说明(新手必懂)
-
缓存优先:查询数据时,先查Redis缓存,缓存存在则直接返回,避免查询数据库;
-
缓存重建:缓存不存在时,查询数据库,将结果存入缓存,下次查询可直接复用;
-
缓存同步:修改、删除数据时,同步删除对应缓存,避免缓存与数据库数据不一致(脏数据);
-
过期策略:给缓存设置过期时间,避免缓存数据一直占用内存,同时减少缓存一致性问题。
第二章:实战进阶------Redis高级用法,覆盖复杂业务场景
掌握了Redis的基础用法和缓存逻辑后,我开始在项目中大量使用Redis,但很快发现,基础用法无法覆盖复杂场景------比如缓存穿透、缓存击穿、缓存雪崩、分布式锁、消息队列等。这时李哥告诉我:"Redis的高级用法,才是它真正的'核心竞争力',能帮你解决高并发、分布式场景下的各种问题,让系统更稳定、更高效。"
新手重点掌握以下6个高级用法,能轻松应对90%的复杂业务场景。
一、缓存三大问题(缓存穿透、击穿、雪崩)------ 必掌握,避免线上故障
高并发场景下,缓存很容易出现三大问题,若不处理,可能导致数据库崩溃、系统瘫痪,新手必须掌握其成因和解决方案。
1. 缓存穿透(最常见)
成因
查询一个"不存在的数据",缓存中没有,数据库中也没有,导致所有请求都穿透到数据库,大量请求直接打在数据库上,导致数据库压力过大,甚至宕机。
比如:查询ID为-1的用户,缓存中没有,数据库中也没有,每次请求都会去查数据库。
解决方案(两种方案结合使用)
-
方案1:缓存空值(推荐):查询数据库后,若结果为空,也将空值存入缓存,设置较短的过期时间(如5分钟),避免后续相同请求穿透到数据库;
-
方案2:布隆过滤器(进阶):提前将所有存在的用户ID、商品ID存入布隆过滤器,查询时先通过布隆过滤器判断ID是否存在,不存在则直接返回,不查询缓存和数据库。
Spring Boot代码示例(缓存空值)
java
public User getUserById(Long id) {
String cacheKey = USER_CACHE_KEY + id;
User user = (User) redisTemplate.opsForValue().get(cacheKey);
if (user != null) {
return user;
}
// 查数据库
user = userMapper.getUserById(id);
if (user != null) {
// 存在,存入缓存,1小时过期
redisTemplate.opsForValue().set(cacheKey, user, 1, TimeUnit.HOURS);
} else {
// 不存在,缓存空值,5分钟过期
redisTemplate.opsForValue().set(cacheKey, null, 5, TimeUnit.MINUTES);
}
return user;
}
2. 缓存击穿
成因
一个"热点数据"的缓存过期了,此时大量请求同时访问该数据,缓存中没有,所有请求都穿透到数据库,导致数据库瞬间压力骤增,出现短暂宕机。
比如:首页热门商品的缓存过期,此时大量用户访问首页,所有请求都去查数据库。
解决方案(三种方案可选)
-
方案1:互斥锁(简单易实现):缓存过期时,只有一个请求能去查询数据库,其他请求等待,查询完成后存入缓存,其他请求再从缓存获取;
-
方案2:缓存预热(推荐):提前将热点数据存入缓存,并设置较长的过期时间,定期更新缓存,避免缓存过期;
-
方案3:过期时间错开:给热点数据设置不同的过期时间(如加随机数),避免大量热点数据同时过期。
Spring Boot代码示例(互斥锁)
java
public User getHotUserById(Long id) {
String cacheKey = USER_CACHE_KEY + id;
String lockKey = "lock:user:" + id; // 互斥锁key
User user = (User) redisTemplate.opsForValue().get(cacheKey);
// 缓存不存在,尝试获取互斥锁
if (user == null) {
// 尝试获取锁(设置30秒过期,避免死锁)
Boolean lock = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(lock)) {
try {
// 获取锁成功,查询数据库
user = userMapper.getUserById(id);
if (user != null) {
redisTemplate.opsForValue().set(cacheKey, user, 1, TimeUnit.HOURS);
} else {
redisTemplate.opsForValue().set(cacheKey, null, 5, TimeUnit.MINUTES);
}
} finally {
// 释放锁(无论查询成功与否,都要释放锁)
redisTemplate.delete(lockKey);
}
} else {
// 未获取到锁,等待100毫秒后重试
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return getHotUserById(id); // 递归重试
}
}
return user;
}
3. 缓存雪崩
成因
大量缓存数据在同一时间过期,导致大量请求同时穿透到数据库,数据库压力瞬间达到峰值,直接宕机,整个系统瘫痪。
比如:凌晨3点,系统批量更新缓存,所有缓存同时过期,此时若有大量用户访问,所有请求都打在数据库上。
解决方案(三种方案结合使用)
-
方案1:过期时间错开:给缓存数据设置过期时间时,加上随机数(如1-5分钟),避免大量缓存同时过期;
-
方案2:缓存预热+定时更新:提前将缓存数据存入Redis,设置较长的过期时间,通过定时任务(如Spring Task)定期更新缓存,避免缓存过期;
-
方案3:多级缓存:搭建"本地缓存(Caffeine)+ 分布式缓存(Redis)",即使Redis缓存过期,本地缓存也能承接部分请求,减少数据库压力。
Spring Boot代码示例(过期时间错开)
java
// 存入缓存时,加上随机过期时间(1小时±5分钟)
int random = new Random().nextInt(10) - 5; // -5到5分钟
long expireTime = 3600 + random * 60; // 单位:秒
redisTemplate.opsForValue().set(cacheKey, user, expireTime, TimeUnit.SECONDS);
二、分布式锁(Redis实现)------ 分布式系统必用
核心场景
分布式系统中,多个服务节点同时操作同一资源(如库存扣减、订单创建),会出现并发问题(如超卖、重复订单),此时需要分布式锁来保证操作的原子性,确保同一时间只有一个节点能操作资源。
李哥给我举了个真实案例:之前项目里做商品秒杀,没有用分布式锁,两个服务节点同时扣减同一商品的库存,导致库存出现负数(超卖),后续花了大量时间排查和回滚数据。"小王,单机系统用synchronized锁就能解决并发问题,但分布式系统中,服务部署在不同节点,synchronized锁只对单个节点有效,跨节点的并发必须用分布式锁,而Redis是实现分布式锁最常用、最简洁的方案。"
Redis实现分布式锁的核心原理
利用Redis的SETNX命令(SET if Not Exists):当key不存在时,设置key-value并返回1(获取锁成功);当key已存在时,直接返回0(获取锁失败)。结合过期时间,避免死锁(防止获取锁的节点宕机,锁无法释放)。
核心要点(新手必记):
-
锁key命名规范:
lock:业务模块:资源标识(如lock:seckill:goods_1001,表示商品秒杀场景中,商品ID为1001的锁); -
必须设置过期时间:避免节点宕机后锁无法释放,导致死锁;
-
原子性操作:获取锁(SETNX+过期时间)必须是原子操作,否则会出现"设置了key但未设置过期时间"的异常场景;
-
释放锁必须校验:只能释放自己获取的锁,避免误释放其他节点的锁(可通过给value设置唯一标识,如UUID实现)。
实战实现(Spring Boot代码,可直接复制使用)
1. 分布式锁工具类(封装获取锁、释放锁方法)
java
package com.example.demo.util;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@Component
public class RedisDistributedLockUtil {
private final RedisTemplate<String, Object> redisTemplate;
// 注入RedisTemplate
public RedisDistributedLockUtil(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
/**
* 获取分布式锁
* @param lockKey 锁的key
* @param expireTime 过期时间(秒)
* @return 锁的唯一标识(value),获取失败返回null
*/
public String getLock(String lockKey, long expireTime) {
// 生成唯一标识(避免误释放其他节点的锁)
String lockValue = UUID.randomUUID().toString();
// 原子操作:SETNX + 过期时间(Redis 2.6.12+支持该语法)
Boolean success = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, expireTime, TimeUnit.SECONDS);
// 获取锁成功,返回唯一标识;失败返回null
return Boolean.TRUE.equals(success) ? lockValue : null;
}
/**
* 释放分布式锁(校验唯一标识,避免误释放)
* @param lockKey 锁的key
* @param lockValue 锁的唯一标识(获取锁时返回的value)
* @return 释放成功返回true,失败返回false
*/
public boolean releaseLock(String lockKey, String lockValue) {
// 获取锁当前的value
String currentValue = (String) redisTemplate.opsForValue().get(lockKey);
// 校验:当前value与传入的lockValue一致,说明是自己获取的锁,才释放
if (lockValue != null && lockValue.equals(currentValue)) {
// 释放锁(删除key)
redisTemplate.delete(lockKey);
return true;
}
return false;
}
}
2. 业务场景实战(商品秒杀,解决超卖问题)
以商品秒杀场景为例,多个服务节点同时扣减商品库存,用分布式锁保证同一时间只有一个节点能执行扣减操作,避免超卖。
java
package com.example.demo.service;
import com.example.demo.mapper.GoodsMapper;
import com.example.demo.util.RedisDistributedLockUtil;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class SeckillService {
private final GoodsMapper goodsMapper;
private final RedisDistributedLockUtil distributedLockUtil;
public SeckillService(GoodsMapper goodsMapper, RedisDistributedLockUtil distributedLockUtil) {
this.goodsMapper = goodsMapper;
this.distributedLockUtil = distributedLockUtil;
}
// 锁key前缀
private static final String SECKILL_LOCK_KEY = "lock:seckill:goods:";
// 锁过期时间(30秒,根据业务调整)
private static final long LOCK_EXPIRE_TIME = 30;
/**
* 商品秒杀(扣减库存)
* @param goodsId 商品ID
* @param userId 用户ID
* @return 秒杀结果(成功/失败)
*/
@Transactional
public String seckill(Long goodsId, Long userId) {
// 1. 构建锁key(锁定当前商品)
String lockKey = SECKILL_LOCK_KEY + goodsId;
String lockValue = null;
try {
// 2. 获取分布式锁(最多重试3次,避免一直等待)
int retryCount = 3;
while (retryCount > 0) {
lockValue = distributedLockUtil.getLock(lockKey, LOCK_EXPIRE_TIME);
if (lockValue != null) {
// 3. 获取锁成功,执行库存扣减逻辑
// 先查询库存(避免超卖,必须在锁内查询)
Integer stock = goodsMapper.getGoodsStock(goodsId);
if (stock == null || stock <= 0) {
return "商品已售罄,秒杀失败!";
}
// 扣减库存(数据库操作)
int rows = goodsMapper.decreaseStock(goodsId);
if (rows > 0) {
// 库存扣减成功,创建秒杀订单(后续可扩展)
return "秒杀成功!商品ID:" + goodsId + ",用户ID:" + userId;
} else {
return "库存扣减失败,秒杀失败!";
}
}
// 获取锁失败,重试(等待100毫秒后重试)
Thread.sleep(100);
retryCount--;
}
// 重试3次仍未获取到锁,返回失败
return "当前秒杀人数过多,请稍后再试!";
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return "秒杀异常,请稍后再试!";
} finally {
// 4. 释放锁(无论成功与否,都要释放,避免死锁)
if (lockValue != null) {
distributedLockUtil.releaseLock(lockKey, lockValue);
}
}
}
}
关键注意事项(新手避坑)
-
锁的过期时间设置:不能太短(避免业务未执行完,锁就过期,导致并发问题),也不能太长(避免节点宕机后,锁长时间无法释放,影响业务),一般设置30秒-5分钟,根据业务执行时间调整;
-
重试机制:获取锁失败后,不要直接返回,可设置重试次数(如3次),每次重试间隔100-500毫秒,提升用户体验;
-
事务与锁的顺序:必须先获取锁,再执行数据库事务,避免事务执行过程中锁过期,导致并发问题;
-
避免死锁:核心是"设置过期时间"+"finally释放锁",双重保障,即使节点宕机,锁也会在过期后自动释放。
李哥补充道:"Redis实现的分布式锁,适合大部分中小规模分布式系统;如果是超大规模、高并发场景(如百万级秒杀),可以用Redisson框架,它封装了更完善的分布式锁实现,支持可重入锁、公平锁、红锁等,还能自动续期,避免锁过期。"
三、Redis消息队列(简单实现)------ 解耦业务,提升系统性能
核心场景
日常开发中,很多业务场景不需要复杂的消息队列(如RabbitMQ、Kafka),此时用Redis的列表(List)结构就能实现简单的消息队列,实现业务解耦、异步处理,提升系统性能。
比如:用户注册后,需要发送短信验证码、推送欢迎通知、初始化用户信息,这些操作不需要同步执行,可通过Redis消息队列异步处理,避免阻塞注册接口,提升用户注册体验。
核心原理
利用Redis列表(List)的LPUSH(从头部添加消息)和RPOP(从尾部消费消息),或RPUSH(从尾部添加消息)和LPOP(从头部消费消息),实现"生产者-消费者"模型:
-
生产者:通过LPUSH/RPUSH将消息添加到列表中;
-
消费者:通过RPOP/LPOP从列表中获取消息并消费;
-
特点:简单易用、轻量级,适合低并发、非核心业务的异步处理;缺点:不支持消息持久化(Redis宕机后消息丢失)、不支持消息确认、不支持死信队列。
实战实现(Spring Boot代码)
1. 消息生产者(用户注册后发送消息)
java
package com.example.demo.service;
import com.example.demo.entity.User;
import com.example.demo.mapper.UserMapper;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class UserRegisterService {
private final UserMapper userMapper;
private final RedisTemplate<String, Object> redisTemplate;
public UserRegisterService(UserMapper userMapper, RedisTemplate<String, Object> redisTemplate) {
this.userMapper = userMapper;
this.redisTemplate = redisTemplate;
}
// 消息队列key(用户注册消息)
private static final String USER_REGISTER_QUEUE = "queue:user:register";
/**
* 用户注册(同步执行)
* @param user 用户信息
* @return 注册结果
*/
@Transactional
public String register(User user) {
// 1. 保存用户信息(数据库操作)
userMapper.insertUser(user);
// 2. 发送注册消息到Redis队列(异步处理后续操作)
redisTemplate.opsForList().rightPush(USER_REGISTER_QUEUE, user.getId());
// 3. 直接返回注册成功,无需等待后续操作
return "注册成功!验证码已发送至您的手机,请查收。";
}
}
2. 消息消费者(异步处理注册后续操作)
通过Spring的@Scheduled定时任务,定期从Redis队列中获取消息并消费,实现异步处理。
java
package com.example.demo.service;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
@Service
public class UserRegisterConsumerService {
private final RedisTemplate<String, Object> redisTemplate;
private final SmsService smsService;
private final UserService userService;
public UserRegisterConsumerService(RedisTemplate<String, Object> redisTemplate, SmsService smsService, UserService userService) {
this.redisTemplate = redisTemplate;
this.smsService = smsService;
this.userService = userService;
}
// 消息队列key(与生产者一致)
private static final String USER_REGISTER_QUEUE = "queue:user:register";
/**
* 定时消费注册消息(每100毫秒执行一次)
* 注:实际开发中,可根据业务并发量调整执行频率
*/
@Scheduled(fixedRate = 100)
public void consumeRegisterMessage() {
try {
// 从队列头部获取消息(LPOP:非阻塞,没有消息返回null)
Long userId = (Long) redisTemplate.opsForList().leftPop(USER_REGISTER_QUEUE);
if (userId != null) {
// 消费消息:执行注册后续操作
// 1. 发送短信验证码
smsService.sendRegisterSms(userId);
// 2. 初始化用户信息(如给用户分配默认角色、积分)
userService.initUserInfo(userId);
// 3. 推送欢迎通知(后续可扩展)
System.out.println("用户" + userId + "注册后续操作执行完成!");
}
} catch (Exception e) {
// 消息消费失败,可将消息重新放入队列(避免消息丢失)
redisTemplate.opsForList().rightPush(USER_REGISTER_QUEUE, e.getMessage());
System.out.println("消息消费失败,已重新放入队列:" + e.getMessage());
}
}
}
3. 补充说明
-
非阻塞消费:上述代码用LPOP实现非阻塞消费,没有消息时直接返回null,不会阻塞线程;
-
阻塞消费:若想实现阻塞消费(没有消息时阻塞,有消息时立即消费),可使用BLPOP命令(阻塞式左弹出),修改消费代码如下:
java
// 阻塞式消费(最多阻塞10秒,没有消息则返回null)
List<Object> message = redisTemplate.opsForList().leftPop(USER_REGISTER_QUEUE, 10, TimeUnit.SECONDS);
if (message != null && !message.isEmpty()) {
Long userId = (Long) message.get(1);
// 消费消息...
}
四、Redis持久化(避免数据丢失)------ 新手必懂的运维基础
核心问题
Redis是基于内存的数据库,一旦Redis服务宕机,内存中的数据会全部丢失,导致缓存失效,所有请求直接穿透到数据库,引发数据库压力骤增。因此,必须开启Redis持久化,将内存中的数据保存到磁盘,Redis重启后可从磁盘恢复数据。
李哥提醒我:"新手很容易忽略Redis持久化,以为只要部署了Redis就万事大吉,一旦Redis宕机,缓存数据全部丢失,项目直接出问题。开启持久化是Redis运维的基础,也是保证缓存可靠性的关键。"
Redis两种持久化方式(新手重点掌握)
Redis提供两种持久化方式,可单独使用,也可结合使用,新手优先掌握RDB方式,再了解AOF方式。
1. RDB(Redis Database)------ 快照持久化(推荐新手使用)
核心原理
在指定的时间间隔内,将Redis内存中的所有数据生成快照(.rdb文件),保存到磁盘,Redis重启后,加载.rdb文件恢复数据。
配置方式(Redis配置文件redis.conf)
conf
# RDB持久化配置(默认开启)
# 格式:save 时间间隔(秒) 数据修改次数
# 含义:在指定时间间隔内,数据修改次数达到指定值,就生成一次快照
save 900 1 # 900秒(15分钟)内,有1次数据修改,生成快照
save 300 10 # 300秒(5分钟)内,有10次数据修改,生成快照
save 60 10000 # 60秒(1分钟)内,有10000次数据修改,生成快照
# RDB文件保存路径(默认当前目录)
dir ./
# RDB文件名称(默认dump.rdb)
dbfilename dump.rdb
# 当Redis无法写入磁盘时,是否停止接收写操作(推荐开启)
stop-writes-on-bgsave-error yes
# RDB文件压缩(推荐开启,节省磁盘空间)
rdbcompression yes
优点与缺点
-
优点:快照文件体积小、恢复速度快,适合大规模数据恢复,对Redis性能影响小(生成快照时,Redis会fork一个子进程,由子进程执行快照操作,不影响主进程处理请求);
-
缺点:数据一致性差,若Redis宕机,最后一次快照之后的数据会丢失(比如15分钟生成一次快照,Redis在10分钟时宕机,这10分钟内的数据会丢失)。
2. AOF(Append Only File)------ 日志持久化(进阶使用)
核心原理
将Redis执行的每一条写命令(如SET、HSET、INCR等),追加到AOF日志文件(.aof文件)中,Redis重启后,重新执行AOF文件中的所有命令,恢复数据。
配置方式(Redis配置文件redis.conf)
conf
# 开启AOF持久化(默认关闭,需手动开启)
appendonly yes
# AOF文件保存路径(与RDB文件路径一致)
dir ./
# AOF文件名称(默认appendonly.aof)
appendfilename "appendonly.aof"
# AOF同步策略(重点,影响性能和数据一致性)
# appendfsync always # 每执行一条写命令,就同步到磁盘(数据最安全,但性能最差)
appendfsync everysec # 每秒同步一次(推荐,兼顾性能和数据一致性,最多丢失1秒数据)
# appendfsync no # 由操作系统决定何时同步(性能最好,但数据一致性最差)
# AOF文件重写(避免文件过大)
auto-aof-rewrite-percentage 100 # 当AOF文件大小达到上次重写后大小的100%(即翻倍),触发重写
auto-aof-rewrite-min-size 64mb # 当AOF文件大小达到64MB,才触发重写(避免小文件频繁重写)
优点与缺点
-
优点:数据一致性好,最多丢失1秒数据(取决于同步策略),AOF文件可读性强(可直接查看执行的命令);
-
缺点:AOF文件体积大、恢复速度慢,对Redis性能影响比RDB大(每执行一条写命令,都要追加到文件)。
新手建议
中小规模项目:开启RDB持久化即可,兼顾性能和数据恢复需求;
大规模、对数据一致性要求高的项目:开启RDB+AOF结合持久化,RDB用于快速恢复,AOF用于补充数据,确保数据尽量不丢失。
五、Redis性能优化(新手可落地的技巧)
掌握了Redis的基础和高级用法后,还要学会优化Redis性能,避免Redis成为系统瓶颈。李哥告诉我:"Redis的性能优化不需要复杂的操作,新手只要做好以下5点,就能大幅提升Redis的性能和稳定性。"
1. 合理设置缓存过期时间
-
避免缓存永不过期:缓存数据永不过期会导致内存占用越来越大,最终导致RedisOOM;
-
过期时间差异化:给不同业务的缓存设置不同的过期时间,避免大量缓存同时过期(缓解缓存雪崩);
-
热点数据过期时间延长:热点数据(如首页数据、热门商品)可设置较长的过期时间,结合定时更新,减少缓存重建频率。
2. 优化key的命名和结构
-
命名规范:
模块:功能:唯一标识(如user:info:1001、goods:stock:1001),避免key冲突,便于维护; -
避免key过长:key过长会占用更多内存,且查询效率会降低,尽量简洁明了;
-
合理使用数据结构:避免用字符串存储复杂对象(如用户信息),优先用哈希(Hash)结构,节省内存,且便于单独修改字段。
3. 优化Redis连接池
Spring Boot 3.2.x默认使用Lettuce连接池,合理配置连接池参数,避免连接泄露、连接不足:
-
max-active:最大连接数,根据业务并发量调整(默认8,高并发场景可设置为50-100);
-
max-idle:最大空闲连接数,建议与max-active一致,避免频繁创建和销毁连接;
-
min-idle:最小空闲连接数,设置为2-5,保证有空闲连接可用;
-
max-wait:连接超时时间,设置为10-30秒,避免线程长时间阻塞。
4. 避免大量大key操作
大key(如存储大量数据的列表、哈希)会导致Redis执行命令时间过长,阻塞主进程,影响整体性能:
-
拆分大key:将大key拆分为多个小key(如将一个存储1000个用户的列表,拆分为10个存储100个用户的列表);
-
避免一次性获取大key:如避免使用HGETALL获取所有字段,优先使用HGET获取需要的字段。
5. 开启Redis集群(高并发场景)
当Redis单机性能无法满足需求(如并发量超过10万QPS),可搭建Redis集群,实现负载均衡、高可用:
-
主从复制:一主多从,主节点负责写操作,从节点负责读操作,提升读性能;
-
哨兵模式:监控主从节点,主节点宕机后,自动将从节点切换为主节点,保证高可用;
-
Redis Cluster:分布式集群,将数据分片存储在多个节点,支持水平扩展,适合大规模数据存储和高并发场景。
第三章:实战踩坑与总结------从新手到熟练,避开这些坑
从一开始被缓存乱象逼到焦头烂额,到现在能熟练用Redis解决分布式缓存、高并发、异步处理等问题,我踩了很多坑,也积累了很多实战经验。李哥常说:"新手学Redis,不仅要掌握用法,更要学会避坑,很多线上故障,都是因为忽略了一些细节。"
一、新手最容易踩的5个坑(必看)
1. 忽略序列化配置,导致缓存乱码
坑点:直接使用RedisTemplate默认的JDK序列化,存储对象时会出现乱码,无法直接查看;
解决方案:自定义RedisTemplate,使用Jackson序列化,如本文第一章配置类所示。
2. 缓存与数据库数据不一致(脏数据)
坑点:修改、删除数据库数据后,忘记更新或删除缓存,导致缓存中存储的是旧数据;
解决方案:遵循"缓存同步"原则,修改、删除数据时,同步删除对应缓存,或更新缓存。
3. 不设置缓存过期时间,导致Redis OOM
坑点:缓存数据永不过期,内存占用越来越大,最终导致Redis服务崩溃;
解决方案:给所有缓存数据设置合理的过期时间,结合过期时间错开,避免缓存雪崩。
4. 分布式锁未释放,导致死锁
坑点:获取锁后,业务执行异常,未在finally中释放锁,导致锁无法释放,其他请求无法获取锁;
解决方案:将释放锁的代码放在finally中,确保无论业务执行成功与否,都能释放锁,同时设置锁的过期时间,双重保障。
5. 滥用Redis,什么数据都往缓存放
坑点:将低频访问数据、超大体积数据(如大文件)存入Redis,浪费内存,影响Redis性能;
解决方案:只缓存高频访问、体积较小的数据(如用户Token、高频查询的商品信息),低频数据直接查询数据库。
二、总结:Redis新手进阶路径
对于Java新手来说,Redis的进阶路径很清晰,按照以下步骤学习,就能快速掌握Redis,并用它为项目赋能:
-
入门:搞懂Redis的定位、核心优势,掌握Spring Boot 3.2.x整合Redis的基础配置(依赖、配置文件、配置类);
-
核心:吃透5种核心数据结构(String、Hash、List、Set、ZSet),掌握每种结构的常用命令和实战场景;
-
实战:在Service层实现"缓存+数据库"的双重存储,掌握缓存优先、缓存重建、缓存同步的核心逻辑;
-
进阶:掌握缓存三大问题(穿透、击穿、雪崩)的解决方案,实现Redis分布式锁、简单消息队列;
-
优化:了解Redis持久化方式,掌握性能优化技巧,避开新手常见坑,确保Redis稳定运行。
李哥说过:"Redis不难,难的是把它用对、用活。新手不要追求复杂的用法,先把基础打牢,结合真实业务场景多练习,慢慢就能熟练掌握,让Redis成为你后端开发的'加分项'。"
最后,希望这篇实战指南,能帮助和曾经的我一样,被缓存问题困扰的Java新手,快速摆脱缓存乱象,学会用Redis高效赋能项目,在后端开发的道路上稳步进阶!