【Redis八股|第10篇】Spring Boot 项目整合 Redis 实战
前言
前面 Redis 系列已经讲了很多理论知识,包括:
- Redis 是什么
- Redis 为什么快
- 五大数据类型
- String 常见命令
- 缓存穿透、击穿、雪崩
- 过期删除和内存淘汰
- RDB 和 AOF
- 分布式锁
- 主从复制、哨兵、集群
这一篇我们回到 Java 项目中,写一个更贴近实际开发的内容:Spring Boot 整合 Redis。
在 Spring Boot 项目中,Redis 常见用途包括:
- 保存验证码
- 缓存用户信息
- 缓存商品详情
- 实现阅读量自增
- 实现简单限流
- 保存登录 token
这一篇我们就以 Spring Boot 项目为例,演示如何引入 Redis、配置 Redis、使用StringRedisTemplate操作 Redis,并实现几个常见功能。
一、引入 Redis 依赖
在 Spring Boot 项目中,先添加 Redis 依赖。
<!-- pom.xml --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency>如果项目中要做 JSON 序列化,可以使用 Jackson:
<dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId></dependency>大多数 Spring Boot Web 项目本身已经包含 Jackson。
二、配置 Redis 连接
在application.yml中配置 Redis。
spring:redis:host:localhostport:6379password:database:0说明:
| 配置 | 含义 |
|---|---|
host | Redis 地址 |
port | Redis 端口 |
password | Redis 密码,没有可不填 |
database | 使用第几个库 |
如果是 Spring Boot 3 或较新版本,配置前缀可能是:
spring:data:redis:host:localhostport:6379database:0具体以项目 Spring Boot 版本为准。
三、StringRedisTemplate 和 RedisTemplate 的区别
Spring Boot 中常用两个模板:
RedisTemplateStringRedisTemplate1. StringRedisTemplate
StringRedisTemplate的 key 和 value 默认都是字符串。
适合操作:
- 字符串
- JSON 字符串
- token
- 验证码
- 计数器
比如:
stringRedisTemplate.opsForValue().set("name","zhangsan");2. RedisTemplate
RedisTemplate更通用,可以操作对象。
但默认序列化方式如果没配置好,可能会出现乱码或不易读的问题。
学习和项目初期,更推荐先使用:
StringRedisTemplate对象可以手动转成 JSON 字符串保存。
这种方式可读性更好,也更容易排查问题。
四、创建统一 Result 返回类
为了和前面的 Spring Boot 系列风格一致,这里继续使用统一返回结果。
publicclassResult{privateIntegercode;privateStringmsg;privateObjectdata;publicResult(){}publicResult(Integercode,Stringmsg,Objectdata){this.code=code;this.msg=msg;this.data=data;}publicstaticResultsuccess(){returnnewResult(1,"success",null);}publicstaticResultsuccess(Objectdata){returnnewResult(1,"success",data);}publicstaticResulterror(Stringmsg){returnnewResult(0,msg,null);}// getter、setter 省略}实际项目中也可以使用泛型版Result<T>。
五、案例一:保存验证码
1. 功能需求
用户输入手机号,请求发送验证码。
后端生成 6 位验证码,保存到 Redis,并设置 5 分钟过期。
key 设计:
login:code:手机号比如:
login:code:13800000000value 保存验证码:
9527132. Controller 层
@RestController@RequestMapping("/code")publicclassCodeController{@AutowiredprivateCodeServicecodeService;@PostMapping("/send")publicResultsendCode(@RequestParamStringphone){codeService.sendCode(phone);returnResult.success();}@PostMapping("/check")publicResultcheckCode(@RequestParamStringphone,@RequestParamStringcode){booleanresult=codeService.checkCode(phone,code);if(result){returnResult.success("验证码正确");}returnResult.error("验证码错误或已过期");}}3. Service 层
publicinterfaceCodeService{voidsendCode(Stringphone);booleancheckCode(Stringphone,Stringcode);}实现类:
@ServicepublicclassCodeServiceImplimplementsCodeService{@AutowiredprivateStringRedisTemplatestringRedisTemplate;@OverridepublicvoidsendCode(Stringphone){Stringcode=String.valueOf(newRandom().nextInt(900000)+100000);Stringkey="login:code:"+phone;stringRedisTemplate.opsForValue().set(key,code,5,TimeUnit.MINUTES);System.out.println("验证码:"+code);}@OverridepublicbooleancheckCode(Stringphone,Stringcode){Stringkey="login:code:"+phone;StringredisCode=stringRedisTemplate.opsForValue().get(key);returncode!=null&&code.equals(redisCode);}}4. 文字说明
这里使用:
stringRedisTemplate.opsForValue().set(key,code,5,TimeUnit.MINUTES);表示把验证码保存到 Redis,并设置 5 分钟过期。
校验时:
stringRedisTemplate.opsForValue().get(key);如果 Redis 中查不到,说明验证码不存在或已经过期。
这个案例体现了 Redis 非常适合保存临时数据。
六、案例二:缓存商品详情
1. 功能需求
查询商品详情时,先查 Redis。
如果 Redis 有缓存,直接返回。
如果 Redis 没有缓存,再查 MySQL,并把查询结果写入 Redis。
key 设计:
product:商品id比如:
product:10012. 商品实体类
publicclassProduct{privateLongid;privateStringname;privateBigDecimalprice;privateIntegerstock;// getter、setter 省略}3. Mapper 层
@MapperpublicinterfaceProductMapper{@Select("select id, name, price, stock from product where id = #{id}")ProductgetById(Longid);}4. Service 层
publicinterfaceProductService{ProductgetById(Longid);}实现类:
@ServicepublicclassProductServiceImplimplementsProductService{@AutowiredprivateProductMapperproductMapper;@AutowiredprivateStringRedisTemplatestringRedisTemplate;@AutowiredprivateObjectMapperobjectMapper;@OverridepublicProductgetById(Longid){Stringkey="product:"+id;Stringjson=stringRedisTemplate.opsForValue().get(key);if(json!=null&&!json.isEmpty()){try{returnobjectMapper.readValue(json,Product.class);}catch(JsonProcessingExceptione){thrownewRuntimeException("商品缓存解析失败",e);}}Productproduct=productMapper.getById(id);if(product==null){stringRedisTemplate.opsForValue().set(key,"",2,TimeUnit.MINUTES);returnnull;}try{StringproductJson=objectMapper.writeValueAsString(product);stringRedisTemplate.opsForValue().set(key,productJson,30,TimeUnit.MINUTES);returnproduct;}catch(JsonProcessingExceptione){thrownewRuntimeException("商品缓存序列化失败",e);}}}5. Controller 层
@RestController@RequestMapping("/products")publicclassProductController{@AutowiredprivateProductServiceproductService;@GetMapping("/{id}")publicResultgetById(@PathVariableLongid){Productproduct=productService.getById(id);if(product==null){returnResult.error("商品不存在");}returnResult.success(product);}}6. 文字说明
这个案例就是典型的缓存查询流程:
先查 Redis Redis 命中:直接返回 Redis 未命中:查 MySQL MySQL 有数据:写入 Redis MySQL 无数据:缓存空值这里还加入了空值缓存:
stringRedisTemplate.opsForValue().set(key,"",2,TimeUnit.MINUTES);这样可以缓解缓存穿透。
如果查询一个不存在的商品 id,短时间内不会反复打到数据库。
七、案例三:阅读量自增
1. 功能需求
每次用户访问文章详情时,文章阅读量加 1。
key 设计:
article:文章id:readCount比如:
article:1001:readCount2. Service 代码
@ServicepublicclassArticleService{@AutowiredprivateStringRedisTemplatestringRedisTemplate;publicLongincrReadCount(LongarticleId){Stringkey="article:"+articleId+":readCount";returnstringRedisTemplate.opsForValue().increment(key);}publicStringgetReadCount(LongarticleId){Stringkey="article:"+articleId+":readCount";returnstringRedisTemplate.opsForValue().get(key);}}3. Controller 代码
@RestController@RequestMapping("/articles")publicclassArticleController{@AutowiredprivateArticleServicearticleService;@PostMapping("/{id}/read")publicResultread(@PathVariableLongid){Longcount=articleService.incrReadCount(id);returnResult.success(count);}@GetMapping("/{id}/readCount")publicResultgetReadCount(@PathVariableLongid){returnResult.success(articleService.getReadCount(id));}}4. 文字说明
这里使用的是 Redis String 的自增能力:
increment(key)它对应 Redis 中的:
incr key适合做:
- 阅读量
- 点赞数
- 接口访问次数
- 短信发送次数
需要注意,如果阅读量最终要持久化到 MySQL,可以定时把 Redis 中的数据同步回数据库。
八、案例四:简单接口限流
1. 功能需求
限制用户 60 秒内最多发送 5 次验证码。
key 设计:
limit:sendCode:手机号比如:
limit:sendCode:138000000002. Service 代码
publicvoidcheckSendCodeLimit(Stringphone){Stringkey="limit:sendCode:"+phone;Longcount=stringRedisTemplate.opsForValue().increment(key);if(count!=null&&count==1){stringRedisTemplate.expire(key,60,TimeUnit.SECONDS);}if(count!=null&&count>5){thrownewRuntimeException("验证码发送过于频繁,请稍后再试");}}在发送验证码前调用:
@OverridepublicvoidsendCode(Stringphone){checkSendCodeLimit(phone);Stringcode=String.valueOf(newRandom().nextInt(900000)+100000);Stringkey="login:code:"+phone;stringRedisTemplate.opsForValue().set(key,code,5,TimeUnit.MINUTES);System.out.println("验证码:"+code);}3. 文字说明
这段代码核心是:
Longcount=stringRedisTemplate.opsForValue().increment(key);每请求一次,计数加 1。
如果是第一次请求:
if(count!=null&&count==1){stringRedisTemplate.expire(key,60,TimeUnit.SECONDS);}设置 60 秒过期。
当 60 秒内请求次数超过 5 次:
thrownewRuntimeException("验证码发送过于频繁,请稍后再试");这就是一个简单固定窗口限流。
九、Redis key 命名规范
项目中 Redis key 不要随便起名。
不推荐:
code123 user1 p1001推荐:
login:code:13800000000 product:1001 article:1001:readCount limit:sendCode:13800000000命名建议:
业务模块:业务对象:唯一标识:属性比如:
article:1001:readCount表示文章 1001 的阅读量。
好处是:
- 可读性强
- 方便排查问题
- 方便按业务分类
- 避免 key 冲突
十、Redis 使用中的常见问题
1. 缓存对象要不要设置过期时间?
一般建议设置。
比如商品详情缓存:
.set(key,json,30,TimeUnit.MINUTES)如果不设置过期时间,缓存可能长期占用内存。
2. 空值缓存过期时间为什么要短?
因为数据库中未来可能新增这条数据。
如果空值缓存时间太长,新增后用户可能还一直查不到。
所以空值缓存一般设置短一点,比如 1 到 5 分钟。
3. 更新数据库后 Redis 怎么办?
常见做法是:
先更新数据库,再删除缓存比如:
productMapper.update(product);stringRedisTemplate.delete("product:"+product.getId());下一次查询时再重新加载缓存。
4. 为什么不直接更新缓存?
直接更新缓存也可以,但复杂对象可能出现一致性问题。
很多项目更倾向于更新数据库后删除缓存,让下次查询自动重建。
十一、全局异常处理补充
前面代码中使用了:
thrownewRuntimeException("验证码发送过于频繁,请稍后再试");实际项目中更推荐自定义业务异常:
publicclassBusinessExceptionextendsRuntimeException{publicBusinessException(Stringmessage){super(message);}}然后配合全局异常处理:
@RestControllerAdvicepublicclassGlobalExceptionHandler{@ExceptionHandler(BusinessException.class)publicResulthandleBusinessException(BusinessExceptione){returnResult.error(e.getMessage());}@ExceptionHandler(Exception.class)publicResulthandleException(Exceptione){returnResult.error("系统异常,请联系管理员");}}这样接口返回更统一。
十二、面试标准回答
如果面试官问:
Spring Boot 项目中怎么使用 Redis?
可以这样回答:
Spring Boot 中可以引入spring-boot-starter-data-redis依赖,然后在配置文件中配置 Redis 的 host、port、password、database 等信息。
项目中常用StringRedisTemplate操作 Redis,适合保存字符串、JSON、验证码、token、计数器等数据。比如验证码可以使用opsForValue().set(key, code, 5, TimeUnit.MINUTES)保存并设置过期时间;商品详情可以先查 Redis,命中就返回,未命中再查 MySQL,并把结果序列化成 JSON 写入 Redis;阅读量可以使用increment实现自增。
使用 Redis 时要注意 key 命名规范、设置合理过期时间、防止缓存穿透、更新数据库后删除缓存,以及不要存储过大的 value。
十三、常见问题总结
1. Spring Boot 操作 Redis 常用哪个类?
常用:
StringRedisTemplate也可以使用:
RedisTemplate学习和常规业务中,StringRedisTemplate更直观。
2. 对象怎么存 Redis?
通常把对象序列化成 JSON 字符串存入 Redis。
读取时再反序列化成对象。
3. 验证码为什么适合放 Redis?
因为验证码是临时数据,并且需要过期时间。
Redis 天然支持 key 过期。
4. 阅读量为什么适合放 Redis?
因为 Redis 支持原子自增,性能高,适合高频计数。
5. 更新数据库后缓存怎么处理?
常见做法是:
更新数据库后删除缓存下次查询时重新加载缓存。
十四、实际开发建议
Spring Boot 整合 Redis 时,可以注意下面几点:
- 使用清晰的 key 命名规范
- 临时数据一定设置过期时间
- 缓存对象建议设置合理 TTL
- 对象缓存可以使用 JSON 字符串
- 数据库查询为空时可以短时间缓存空值
- 更新数据库后删除缓存
- 不要把超大对象放入 Redis
- 计数器可以使用 Redis 自增命令
- 限流可以使用 Redis 自增加过期时间
- 复杂分布式锁建议用 Redisson
十五、总结
这一篇主要学习了 Spring Boot 项目整合 Redis 的常见用法。
我们先引入 Redis 依赖,配置 Redis 地址,然后使用StringRedisTemplate操作 Redis。
文章中实现了几个常见场景:验证码保存和校验、商品详情缓存、阅读量自增、简单接口限流。
这些功能基本覆盖了 Redis 在 Java 后端项目中的基础用法。真正项目中,Redis 不只是一个缓存工具,还可以承担临时数据存储、计数、限流、分布式锁等职责。
到这里,Redis 八股与实战系列的 10 篇文章就形成了一个完整闭环:从 Redis 基础概念,到数据类型,再到缓存问题、持久化、高可用和项目整合。
