Spring Boot整合Redis:从0到1打造电商系统高性能缓存方案
Spring Boot整合Redis:从0到1打造电商系统高性能缓存方案
一、开篇:一个真实的电商秒杀场景
还记得双11那天吗?你守在手机前,零点一到就疯狂点击"立即抢购",结果页面转了半天,最后告诉你"系统繁忙,请稍后再试"。
作为开发者的你,可能知道背后的原因:流量太大了,数据库扛不住了!
想象一下,一个热门商品有10万人在抢,每秒1万次请求直接打到MySQL数据库,即使你的SQL写得再好,数据库也会瞬间崩溃。
这时候,Redis 就像一位"超级救火队员",能够扛住99%的请求,只让1%的真实订单请求打到数据库。这就是今天要讲的——缓存技术。
二、什么是Redis?用大白话讲给你听
2.1 Redis是什么?
Redis = Remote Dictionary Server(远程字典服务器)
别被这个英文名吓到,你可以把它理解成:
Redis就是一台超级快的内存数据库,就像你电脑的记事本,但是这个记事本读写速度极快,而且能存很多种类型的数据。
2.2 为什么要用Redis?
来,我们用一个生活中的类比:
| 场景 | MySQL | Redis | |------|-------|-------| | 类比 | 图书馆(找书要走很远) | 书包(伸手就能拿到) | | 存储位置 | 硬盘(慢) | 内存(快) | | 读取速度 | 毫秒级 | 微秒级(快1000倍) | | 容量 | 大(TB级) | 小(GB级) | | 成本 | 低 | 高 |
一句话总结:Redis就是把热点数据放在内存里,让查询速度提升1000倍!
2.3 Redis能存什么?
Redis支持多种数据类型,就像你的书包里有不同的隔层:
- String(字符串):存商品名称、价格等简单信息
- Hash(哈希):存商品详情(多个字段)
- List(列表):存商品评论、排行榜
- Set(集合):存商品标签、点赞用户
- ZSet(有序集合):存商品销量排行
三、为什么电商系统必须用Redis?
3.1 传统方案的问题
// ❌ 传统方案:每次都查数据库
@GetMapping("/product/{id}")
public Product getProduct(@PathVariable Long id) {
// 每次请求都查数据库,高并发下数据库会崩溃
return productMapper.selectById(id);
}
问题:
- 10万次请求 = 10万次数据库查询 = 数据库崩溃
- 每次查询都要走磁盘IO,速度慢
- 数据库连接池资源有限
3.2 Redis缓存方案的优势
// ✅ 优化方案:先查缓存,缓存没有再查数据库
@GetMapping("/product/{id}")
public Product getProduct(@PathVariable Long id) {
// 1. 先查Redis缓存(内存,速度快)
Product product = redisTemplate.opsForValue().get("product:" + id);
if (product != null) {
return product; // 缓存命中,直接返回
}
// 2. 缓存没有,查数据库
product = productMapper.selectById(id);
// 3. 查到后写入缓存
redisTemplate.opsForValue().set("product:" + id, product, 30, TimeUnit.MINUTES);
return product;
}
优势:
- 10万次请求 = 1次数据库查询(缓存未命中)+ 99999次缓存命中
- 内存读取速度快1000倍
- 数据库压力降低99%
四、Spring Boot整合Redis:手把手教你实现
4.1 第一步:添加依赖
<!-- pom.xml -->
<dependencies>
<!-- Spring Boot Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 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>
<!-- Lombok(简化代码) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
4.2 第二步:配置文件
# application.yml
server:
port: 8080
spring:
# Redis配置
redis:
host: localhost # Redis服务器地址
port: 6379 # Redis端口
password: # 密码(如果有)
database: 0 # 数据库索引(0-15)
timeout: 3000ms # 连接超时时间
lettuce:
pool:
max-active: 8 # 最大连接数
max-wait: -1ms # 最大等待时间
max-idle: 8 # 最大空闲连接
min-idle: 0 # 最小空闲连接
4.3 第三步:配置Redis序列化(重要!)
package com.example.redis.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配置类
* 解决Redis存储乱码问题
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// Key使用String序列化
StringRedisSerializer stringSerializer = new StringRedisSerializer();
template.setKeySerializer(stringSerializer);
template.setHashKeySerializer(stringSerializer);
// Value使用JSON序列化
GenericJackson2JsonRedisSerializer jsonSerializer =
new GenericJackson2JsonRedisSerializer();
template.setValueSerializer(jsonSerializer);
template.setHashValueSerializer(jsonSerializer);
template.afterPropertiesSet();
return template;
}
}
4.4 第四步:实体类
package com.example.redis.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* 商品实体类
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Product implements Serializable {
private Long id; // 商品ID
private String name; // 商品名称
private BigDecimal price; // 商品价格
private Integer stock; // 库存数量
private String description; // 商品描述
private String imageUrl; // 商品图片
}
4.5 第五步:商品服务类(核心代码)
package com.example.redis.service;
import com.example.redis.entity.Product;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.concurrent.TimeUnit;
/**
* 商品服务类
* 演示Redis缓存的使用
*/
@Slf4j
@Service
public class ProductService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// 模拟数据库存储
private static final Product MOCK_PRODUCT = new Product(
1L, "iPhone 15 Pro", new BigDecimal("7999"), 100,
"苹果最新旗舰手机", "https://example.com/iphone.jpg"
);
private static final String PRODUCT_KEY_PREFIX = "product:";
private static final long CACHE_EXPIRE_TIME = 30; // 缓存30分钟
/**
* 获取商品信息(带缓存)
* 先查Redis,没有再查数据库
*/
public Product getProductById(Long id) {
String key = PRODUCT_KEY_PREFIX + id;
// 1. 先查Redis缓存
Product product = (Product) redisTemplate.opsForValue().get(key);
if (product != null) {
log.info("✅ 缓存命中,商品信息从Redis获取:{}", product.getName());
return product;
}
// 2. 缓存未命中,查询数据库(这里用模拟数据)
log.info("❌ 缓存未命中,从数据库查询商品信息");
product = MOCK_PRODUCT;
// 3. 写入缓存,设置过期时间
redisTemplate.opsForValue().set(key, product, CACHE_EXPIRE_TIME, TimeUnit.MINUTES);
log.info("💾 商品信息已写入Redis缓存,过期时间:{}分钟", CACHE_EXPIRE_TIME);
return product;
}
/**
* 更新商品信息
* 更新数据库后,同时删除缓存
*/
public void updateProduct(Product product) {
// 1. 更新数据库
log.info("📝 更新数据库中的商品信息");
// productMapper.updateById(product);
// 2. 删除缓存(保证数据一致性)
String key = PRODUCT_KEY_PREFIX + product.getId();
redisTemplate.delete(key);
log.info("🗑️ 已删除Redis缓存:{}", key);
}
/**
* 删除商品
* 删除数据库后,同时删除缓存
*/
public void deleteProduct(Long id) {
// 1. 删除数据库
log.info("📝 删除数据库中的商品信息");
// productMapper.deleteById(id);
// 2. 删除缓存
String key = PRODUCT_KEY_PREFIX + id;
redisTemplate.delete(key);
log.info("🗑️ 已删除Redis缓存:{}", key);
}
/**
* 预热缓存
* 系统启动时,将热点数据提前加载到缓存
*/
public void warmUpCache(Long productId) {
String key = PRODUCT_KEY_PREFIX + productId;
Product product = MOCK_PRODUCT;
redisTemplate.opsForValue().set(key, product, CACHE_EXPIRE_TIME, TimeUnit.MINUTES);
log.info("🔥 缓存预热完成,商品ID:{}", productId);
}
}
4.6 第六步:控制器类
package com.example.redis.controller;
import com.example.redis.entity.Product;
import com.example.redis.service.ProductService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* 商品控制器
*/
@Slf4j
@RestController
@RequestMapping("/api/product")
public class ProductController {
@Autowired
private ProductService productService;
/**
* 获取商品详情
*/
@GetMapping("/{id}")
public Product getProduct(@PathVariable Long id) {
return productService.getProductById(id);
}
/**
* 更新商品
*/
@PutMapping
public String updateProduct(@RequestBody Product product) {
productService.updateProduct(product);
return "更新成功";
}
/**
* 删除商品
*/
@DeleteMapping("/{id}")
public String deleteProduct(@PathVariable Long id) {
productService.deleteProduct(id);
return "删除成功";
}
/**
* 缓存预热
*/
@PostMapping("/warmup/{id}")
public String warmUpCache(@PathVariable Long id) {
productService.warmUpCache(id);
return "缓存预热成功";
}
}
4.7 第七步:启动类
package com.example.redis;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class RedisApplication {
public static void main(String[] args) {
SpringApplication.run(RedisApplication.class, args);
}
}
4.8 测试接口
# 1. 查询商品(第一次,缓存未命中)
curl http://localhost:8080/api/product/1
# 2. 查询商品(第二次,缓存命中)
curl http://localhost:8080/api/product/1
# 3. 更新商品(会删除缓存)
curl -X PUT http://localhost:8080/api/product \
-H "Content-Type: application/json" \
-d '{"id":1,"name":"iPhone 15 Pro Max","price":8999,"stock":50}'
# 4. 缓存预热
curl -X POST http://localhost:8080/api/product/warmup/1
五、实战踩坑:Redis缓存三大经典问题
5.1 缓存穿透
问题场景:
恶意用户大量查询不存在的商品ID(比如ID=-1),这些请求都会穿透Redis,直接打到数据库。
// ❌ 有问题的代码
public Product getProductById(Long id) {
Product product = (Product) redisTemplate.opsForValue().get("product:" + id);
if (product != null) {
return product;
}
// 数据库查不到,返回null
product = productMapper.selectById(id);
return product;
}
解决方案:缓存空值
// ✅ 解决方案
public Product getProductById(Long id) {
String key = "product:" + id;
Product product = (Product) redisTemplate.opsForValue().get(key);
if (product != null) {
// 即使缓存的是null对象,也直接返回
return product;
}
// 查询数据库
product = productMapper.selectById(id);
if (product == null) {
// 数据库查不到,缓存一个空值,防止穿透
redisTemplate.opsForValue().set(key, "NULL", 5, TimeUnit.MINUTES);
return null;
}
// 查到了,缓存真实数据
redisTemplate.opsForValue().set(key, product, 30, TimeUnit.MINUTES);
return product;
}
5.2 缓存雪崩
问题场景:
大量缓存的过期时间设置相同(都是30分钟),30分钟后这些缓存同时失效,导致大量请求瞬间打到数据库。
// ❌ 有问题的代码:所有缓存都是30分钟过期
redisTemplate.opsForValue().set(key, product, 30, TimeUnit.MINUTES);
解决方案:随机过期时间
// ✅ 解决方案:过期时间加随机值
public Product getProductById(Long id) {
String key = "product:" + id;
Product product = (Product) redisTemplate.opsForValue().get(key);
if (product != null) {
return product;
}
product = productMapper.selectById(id);
// 基础过期时间30分钟 + 随机0-10分钟
long randomExpire = 30 + (long) (Math.random() * 10);
redisTemplate.opsForValue().set(key, product, randomExpire, TimeUnit.MINUTES);
return product;
}
5.3 缓存击穿
问题场景:
某个热点商品(比如iPhone)的缓存过期瞬间,大量请求同时查询这个商品,导致数据库瞬间压力激增。
解决方案:互斥锁
// ✅ 解决方案:使用Redis分布式锁
public Product getProductById(Long id) {
String key = "product:" + id;
String lockKey = "lock:product:" + id;
// 1. 查询缓存
Product product = (Product) redisTemplate.opsForValue().get(key);
if (product != null) {
return product;
}
// 2. 尝试获取锁
Boolean lockAcquired = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(lockAcquired)) {
try {
// 3. 获取到锁,再次检查缓存(双重检查)
product = (Product) redisTemplate.opsForValue().get(key);
if (product != null) {
return product;
}
// 4. 查询数据库
product = productMapper.selectById(id);
// 5. 写入缓存
redisTemplate.opsForValue().set(key, product, 30, TimeUnit.MINUTES);
return product;
} finally {
// 6. 释放锁
redisTemplate.delete(lockKey);
}
} else {
// 7. 没获取到锁,等待100ms后重试
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return getProductById(id); // 递归重试
}
}
六、延伸阅读:进阶学习方向
掌握了Redis缓存基础后,你可以继续学习:
6.1 Redis进阶特性
- Redis持久化:RDB和AOF的区别
- Redis集群:主从复制、哨兵模式、Cluster集群
- Redis事务:MULTI、EXEC、WATCH
- Lua脚本:原子性操作
6.2 分布式锁
- Redis分布式锁的实现原理
- Redisson框架的使用
- 看门狗机制
6.3 缓存设计模式
- Cache-Aside:旁路缓存(本文讲的)
- Read-Through:应用代码只读缓存
- Write-Through:写缓存时同步写数据库
- Write-Behind:异步写数据库
6.4 推荐学习资源
- 官方文档:https://redis.io/docs/
- 书籍:《Redis实战》、《Redis设计与实现》
- 视频教程:B站搜索"Redis教程"
七、总结
今天我们从电商秒杀场景出发,学习了:
- 什么是Redis:内存数据库,速度快1000倍
- 为什么用Redis:减轻数据库压力,提升系统性能
- 怎么用Redis:Spring Boot集成Redis的完整代码示例
- 常见问题:缓存穿透、缓存雪崩、缓存击穿的解决方案
记住一句话:Redis就像系统的"外挂内存",用好了能让你的系统性能提升10倍!
💡 最后的小建议:
- 不要为了用Redis而用Redis,先分析你的系统是否真的需要
- 缓存不是银弹,要考虑数据一致性问题
- 多看日志,多监控,及时发现问题
如果这篇文章对你有帮助,记得点赞收藏哦!有问题欢迎在评论区讨论~ 🎉
作者简介:一名热爱分享的Java开发者,专注后端技术,喜欢用大白话讲解复杂技术。关注我,一起学习进步!
更多推荐



所有评论(0)