每天5分钟,掌握一个SpringBoot实战技能。大家好,我是SpringBoot指南的小坏。从今天开始,我们进入第二周的实战进阶篇,首先解决缓存这个"老大难"问题!
零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。
一、真实案例:缓存引发的"血案"
零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。
上个月,我们公司搞了一个"1元秒杀"活动:
活动开始前:
- 预估流量:10万QPS
- Redis集群:准备充分
- 数据库:加了索引,备了从库
活动开始瞬间:
- 0点00分:正常
- 0点01分:Redis挂了!
- 0点02分:数据库挂了!
- 0点05分:整个系统挂了!
原因分析:
- 热点商品被瞬间缓存击穿
- 大量请求直接打到数据库
- 数据库连接池撑爆
- Redis重启后,缓存雪崩
损失:
- 活动失败,用户投诉
- 数据库恢复用了2小时
- 公司品牌受损
今天,我就用实战教会你如何避免这种悲剧!
二、Redis快速集成
2.1 3分钟搞定Redis
第一步:加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
第二步:配连接
# application.yml
spring:
redis:
host: localhost
port: 6379
password: 123456 # 生产环境一定要设密码!
database: 0
# 连接池配置(关键!)
lettuce:
pool:
max-active: 20 # 最大连接数
max-idle: 10 # 最大空闲连接
min-idle: 5 # 最小空闲连接
max-wait: 3000ms # 获取连接最大等待时间
第三步:直接使用
@RestController
public class TestController {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@GetMapping("/test")
public String test() {
// 存数据
redisTemplate.opsForValue().set("key", "value");
// 取数据
String value = redisTemplate.opsForValue().get("key");
return "Redis测试成功,值:" + value;
}
}
三、缓存穿透:查不存在的数据
零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。
3.1 什么是缓存穿透?
场景:
用户请求一个不存在的商品ID,比如:/product/999999
过程:
- 查缓存 → 没有
- 查数据库 → 也没有
- 返回空结果
问题: 如果大量请求不存在的ID,每次都会打到数据库,数据库压力巨大!
3.2 解决方案:缓存空对象
@Service
public class ProductService {
@Autowired
private ProductRepository productRepository;
@Autowired
private RedisTemplate<String, Product> redisTemplate;
/**
* 防穿透:缓存空对象
*/
public Product getProductSafe(Long productId) {
String cacheKey = "product:" + productId;
// 1. 先查缓存
Product product = redisTemplate.opsForValue().get(cacheKey);
// 2. 缓存有数据
if (product != null) {
// 如果是空对象标记,返回null
if (isNullObject(product)) {
return null;
}
return product;
}
// 3. 缓存没有,查数据库
product = productRepository.findById(productId).orElse(null);
// 4. 数据库有,缓存正常数据
if (product != null) {
redisTemplate.opsForValue().set(cacheKey, product, 30, TimeUnit.MINUTES);
return product;
}
// 5. 数据库没有,缓存空对象(防止穿透)
Product nullProduct = createNullObject();
redisTemplate.opsForValue().set(cacheKey, nullProduct, 5, TimeUnit.MINUTES); // 短时间缓存
return null;
}
/**
* 创建空对象标记
*/
private Product createNullObject() {
Product product = new Product();
product.setId(-1L); // 用特殊ID标识空对象
product.setName("NULL_OBJECT");
return product;
}
/**
* 判断是否是空对象标记
*/
private boolean isNullObject(Product product) {
return product != null && product.getId() != null && product.getId() == -1L;
}
}
3.3 增强方案:布隆过滤器
@Component
public class BloomFilterService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 初始化布隆过滤器
*/
public void initBloomFilter() {
String key = "bloom:product:ids";
// 从数据库加载所有存在的商品ID
List<Long> allProductIds = productRepository.findAllIds();
// 将每个ID添加到布隆过滤器
for (Long id : allProductIds) {
add(key, String.valueOf(id));
}
}
/**
* 判断元素是否存在
*/
public boolean mightContain(String key, String value) {
long[] hashIndexes = getHashIndexes(value);
// 检查所有位是否都为1
for (long index : hashIndexes) {
Boolean bit = redisTemplate.opsForValue().getBit(key, index);
if (bit == null || !bit) {
return false;
}
}
return true;
}
/**
* 添加元素到布隆过滤器
*/
public void add(String key, String value) {
long[] hashIndexes = getHashIndexes(value);
for (long index : hashIndexes) {
redisTemplate.opsForValue().setBit(key, index, true);
}
}
/**
* 计算哈希位置(模拟多个哈希函数)
*/
private long[] getHashIndexes(String value) {
// 实际可以用多个哈希函数
// 这里简化处理
long hash1 = Math.abs(value.hashCode());
long hash2 = Math.abs(value.hashCode() * 31);
// 映射到0-99999的范围
return new long[] { hash1 % 100000, hash2 % 100000 };
}
}
// 使用布隆过滤器
@Service
public class ProductServiceWithBloom {
@Autowired
private BloomFilterService bloomFilterService;
public Product getProductWithBloom(Long productId) {
// 先用布隆过滤器判断
boolean mightExist = bloomFilterService.mightContain(
"bloom:product:ids",
String.valueOf(productId)
);
if (!mightExist) {
// 一定不存在,直接返回null
return null;
}
// 可能存在,继续正常流程
return getProductSafe(productId);
}
}
四、缓存击穿:热点key过期
零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。
4.1 什么是缓存击穿?
场景: 一个热点商品(比如:iPhone 15)的缓存过期了,瞬间有1万个请求同时来查这个商品。
过程:
- 缓存过期
- 1万个请求同时发现缓存没有
- 1万个请求同时去查数据库
- 数据库瞬间被打爆
4.2 解决方案:互斥锁
@Service
public class ProductServiceWithLock {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 使用互斥锁防止缓存击穿
*/
public Product getProductWithLock(Long productId) {
String cacheKey = "product:" + productId;
String lockKey = "lock:product:" + productId;
// 1. 先查缓存
Product product = (Product) redisTemplate.opsForValue().get(cacheKey);
if (product != null && !isNullObject(product)) {
return product;
}
// 2. 尝试获取锁
Boolean locked = tryLock(lockKey);
try {
if (locked) {
// 3. 拿到锁,查数据库
product = productRepository.findById(productId).orElse(null);
if (product != null) {
// 4. 写缓存,设置随机过期时间(防雪崩)
int expireSeconds = 1800 + new Random().nextInt(600); // 30-40分钟
redisTemplate.opsForValue().set(
cacheKey, product, expireSeconds, TimeUnit.SECONDS
);
} else {
// 缓存空对象
Product nullProduct = createNullObject();
redisTemplate.opsForValue().set(
cacheKey, nullProduct, 300, TimeUnit.SECONDS
);
}
} else {
// 5. 没拿到锁,等待50ms后重试
Thread.sleep(50);
return getProductWithLock(productId);
}
} finally {
// 6. 释放锁
if (locked) {
releaseLock(lockKey);
}
}
return product;
}
/**
* 尝试获取锁(setnx实现)
*/
private Boolean tryLock(String lockKey) {
// 使用setnx命令,只有key不存在时才能设置成功
return redisTemplate.opsForValue().setIfAbsent(
lockKey,
"1",
10, TimeUnit.SECONDS // 10秒自动过期,防止死锁
);
}
/**
* 释放锁
*/
private void releaseLock(String lockKey) {
redisTemplate.delete(lockKey);
}
}
4.3 更简单的方案:永不过期 + 后台更新
@Service
public class ProductServiceWithBackgroundRefresh {
@Autowired
private RedisTemplate<String, Product> redisTemplate;
/**
* 方案:缓存永不过期,后台定时更新
*/
public Product getProductWithBackgroundRefresh(Long productId) {
String cacheKey = "product:" + productId;
// 1. 先查缓存
Product product = redisTemplate.opsForValue().get(cacheKey);
if (product == null) {
// 2. 缓存没有,查数据库并写入缓存(永不过期)
product = productRepository.findById(productId).orElse(null);
if (product != null) {
redisTemplate.opsForValue().set(cacheKey, product);
// 记录需要更新的时间
recordUpdateTime(productId);
}
} else {
// 3. 检查是否需要更新
if (needRefresh(productId)) {
// 异步更新缓存
refreshCacheInBackground(productId);
}
}
return product;
}
/**
* 异步更新缓存
*/
@Async
public void refreshCacheInBackground(Long productId) {
String cacheKey = "product:" + productId;
try {
Product freshProduct = productRepository.findById(productId).orElse(null);
if (freshProduct != null) {
redisTemplate.opsForValue().set(cacheKey, freshProduct);
updateRefreshTime(productId);
}
} catch (Exception e) {
log.error("后台更新缓存失败,productId: {}", productId, e);
}
}
/**
* 判断是否需要更新
*/
private boolean needRefresh(Long productId) {
// 根据最后更新时间判断,比如超过30分钟
Long lastUpdateTime = getLastUpdateTime(productId);
if (lastUpdateTime == null) {
return true;
}
long now = System.currentTimeMillis();
return now - lastUpdateTime > 30 * 60 * 1000; // 30分钟
}
}
五、缓存雪崩:大量key同时过期
5.1 什么是缓存雪崩?
场景: 大量缓存在同一时间过期,比如:半夜12点,所有缓存设置的是24小时过期。
过程:
- 00:00:大量缓存同时过期
- 大量请求同时打到数据库
- 数据库瞬间崩溃
5.2 解决方案:随机过期时间
@Service
public class ProductServiceWithRandomExpire {
@Autowired
private RedisTemplate<String, Product> redisTemplate;
/**
* 设置缓存,使用随机过期时间
*/
public void setProductWithRandomExpire(Long productId, Product product) {
String cacheKey = "product:" + productId;
// 基础过期时间:30分钟
int baseExpireSeconds = 30 * 60; // 1800秒
// 随机增加0-600秒(0-10分钟)
int randomAddition = new Random().nextInt(600);
int totalExpireSeconds = baseExpireSeconds + randomAddition;
redisTemplate.opsForValue().set(
cacheKey,
product,
totalExpireSeconds,
TimeUnit.SECONDS
);
}
/**
* 批量设置缓存,分散过期时间
*/
public void batchSetProductsWithRandomExpire(Map<Long, Product> productMap) {
List<Long> productIds = new ArrayList<>(productMap.keySet());
// 打乱顺序,让相邻的商品ID过期时间分散
Collections.shuffle(productIds);
for (Long productId : productIds) {
Product product = productMap.get(productId);
setProductWithRandomExpire(productId, product);
}
}
}
5.3 增强方案:缓存预热
零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。
@Component
public class CacheWarmUp {
@Autowired
private ProductRepository productRepository;
@Autowired
private RedisTemplate<String, Product> redisTemplate;
/**
* 系统启动时预热缓存
*/
@PostConstruct
public void warmUpCache() {
log.info("开始缓存预热...");
// 1. 查询热点商品(比如:销量前1000的商品)
List<Product> hotProducts = productRepository.findHotProducts(1000);
// 2. 分批写入缓存
int batchSize = 100;
for (int i = 0; i < hotProducts.size(); i += batchSize) {
int end = Math.min(i + batchSize, hotProducts.size());
List<Product> batch = hotProducts.subList(i, end);
// 异步写入,不影响启动速度
writeBatchToCache(batch);
}
log.info("缓存预热完成,共预热{}个商品", hotProducts.size());
}
/**
* 定时更新缓存
*/
@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行
public void scheduledCacheRefresh() {
log.info("开始定时缓存更新...");
// 1. 查询需要更新的商品
List<Product> productsToUpdate = productRepository.findProductsUpdatedToday();
// 2. 更新缓存
for (Product product : productsToUpdate) {
String cacheKey = "product:" + product.getId();
redisTemplate.opsForValue().set(
cacheKey,
product,
30 + new Random().nextInt(10),
TimeUnit.MINUTES
);
}
log.info("定时缓存更新完成,共更新{}个商品", productsToUpdate.size());
}
@Async
public void writeBatchToCache(List<Product> products) {
for (Product product : products) {
String cacheKey = "product:" + product.getId();
redisTemplate.opsForValue().set(
cacheKey,
product,
30 + new Random().nextInt(10), // 30-40分钟
TimeUnit.MINUTES
);
}
}
}
六、分布式锁实战:商品秒杀
零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。
6.1 秒杀场景分析
需求:
- 1000件商品
- 10万人同时抢
- 不能超卖
- 高性能
问题:
- 库存扣减的并发问题
- 防止重复购买
- 保证公平性
6.2 基于Redis的分布式锁实现
@Service
@Slf4j
public class SeckillService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private ProductService productService;
@Autowired
private OrderService orderService;
/**
* 秒杀抢购(使用分布式锁)
*/
public SeckillResult seckill(Long productId, Long userId) {
String lockKey = "lock:seckill:" + productId;
String stockKey = "stock:" + productId;
String boughtKey = "bought:" + productId + ":" + userId;
// 1. 检查是否已经购买过
Boolean bought = redisTemplate.hasKey(boughtKey);
if (bought != null && bought) {
return SeckillResult.fail("您已经购买过了");
}
// 2. 获取分布式锁
String requestId = UUID.randomUUID().toString();
boolean locked = false;
try {
// 尝试获取锁,最多等待100ms
locked = tryLock(lockKey, requestId, 100);
if (!locked) {
return SeckillResult.fail("系统繁忙,请稍后重试");
}
// 3. 检查库存
String stockStr = redisTemplate.opsForValue().get(stockKey);
if (stockStr == null) {
// 初始化库存到Redis
Product product = productService.getProduct(productId);
if (product == null) {
return SeckillResult.fail("商品不存在");
}
stockStr = String.valueOf(product.getStock());
redisTemplate.opsForValue().set(stockKey, stockStr);
}
int stock = Integer.parseInt(stockStr);
if (stock <= 0) {
return SeckillResult.fail("商品已售罄");
}
// 4. 扣减库存
Long newStock = redisTemplate.opsForValue().decrement(stockKey);
if (newStock < 0) {
// 库存不足,恢复库存
redisTemplate.opsForValue().increment(stockKey);
return SeckillResult.fail("商品已售罄");
}
// 5. 记录购买状态(防止重复购买)
redisTemplate.opsForValue().set(boughtKey, "1", 24, TimeUnit.HOURS);
// 6. 创建订单(异步)
orderService.createSeckillOrderAsync(productId, userId);
return SeckillResult.success("抢购成功");
} finally {
// 7. 释放锁
if (locked) {
releaseLock(lockKey, requestId);
}
}
}
/**
* 尝试获取分布式锁
*/
private boolean tryLock(String lockKey, String requestId, long waitMillis) {
long start = System.currentTimeMillis();
while (true) {
// 使用setnx命令尝试获取锁
Boolean success = redisTemplate.opsForValue().setIfAbsent(
lockKey,
requestId,
10, TimeUnit.SECONDS // 10秒自动过期,防止死锁
);
if (success != null && success) {
return true;
}
// 检查是否超时
if (System.currentTimeMillis() - start > waitMillis) {
return false;
}
// 等待50ms后重试
try {
Thread.sleep(50);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
}
/**
* 释放分布式锁(使用Lua脚本保证原子性)
*/
private void releaseLock(String lockKey, String requestId) {
String luaScript = """
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
""";
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setScriptText(luaScript);
script.setResultType(Long.class);
redisTemplate.execute(script, Collections.singletonList(lockKey), requestId);
}
}
6.3 使用Redisson简化分布式锁
<!-- 添加Redisson依赖 -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.23.2</version>
</dependency>
@Service
public class SeckillServiceWithRedisson {
@Autowired
private RedissonClient redissonClient;
/**
* 使用Redisson分布式锁(更简单可靠)
*/
public SeckillResult seckillWithRedisson(Long productId, Long userId) {
String lockKey = "lock:seckill:" + productId;
String stockKey = "stock:" + productId;
RLock lock = redissonClient.getLock(lockKey);
try {
// 尝试加锁,最多等待100ms,锁持有时间10秒
boolean locked = lock.tryLock(100, 10000, TimeUnit.MILLISECONDS);
if (!locked) {
return SeckillResult.fail("系统繁忙,请稍后重试");
}
// 执行业务逻辑...
RAtomicLong stock = redissonClient.getAtomicLong(stockKey);
// 扣减库存
if (stock.decrementAndGet() < 0) {
stock.incrementAndGet(); // 恢复库存
return SeckillResult.fail("商品已售罄");
}
// 创建订单...
return SeckillResult.success("抢购成功");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return SeckillResult.fail("系统异常");
} finally {
// 释放锁
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
七、缓存一致性:先更新数据库还是先删缓存?
零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。
7.1 两种方案对比
方案一:先更新数据库,再删除缓存
public void updateProduct(Product product) {
// 1. 更新数据库
productRepository.update(product);
// 2. 删除缓存
String cacheKey = "product:" + product.getId();
redisTemplate.delete(cacheKey);
}
问题:删除缓存可能失败,导致缓存是旧数据
方案二:先删除缓存,再更新数据库
public void updateProduct(Product product) {
String cacheKey = "product:" + product.getId();
// 1. 删除缓存
redisTemplate.delete(cacheKey);
// 2. 更新数据库
productRepository.update(product);
}
问题:更新数据库期间,可能有请求读到旧数据并重新缓存
7.2 最佳实践:延迟双删
@Service
public class ProductServiceWithDoubleDelete {
@Autowired
private ProductRepository productRepository;
@Autowired
private RedisTemplate<String, Product> redisTemplate;
@Autowired
private ThreadPoolTaskExecutor taskExecutor;
/**
* 延迟双删策略
*/
public void updateProductWithDoubleDelete(Product product) {
String cacheKey = "product:" + product.getId();
// 1. 第一次删除缓存
redisTemplate.delete(cacheKey);
// 2. 更新数据库
productRepository.update(product);
// 3. 异步延迟第二次删除缓存
taskExecutor.execute(() -> {
try {
// 延迟500ms,等数据库主从同步完成
Thread.sleep(500);
// 第二次删除缓存
redisTemplate.delete(cacheKey);
log.info("延迟双删完成,productId: {}", product.getId());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
}
7.3 终极方案:监听数据库Binlog
@Component
public class CacheSyncWithBinlog {
@Autowired
private RedisTemplate<String, Product> redisTemplate;
/**
* 监听数据库变更,实时更新缓存
*/
@EventListener
public void onDatabaseChange(DatabaseChangeEvent event) {
if (event.getTableName().equals("product")) {
Long productId = event.getRowId();
String cacheKey = "product:" + productId;
if (event.getOperation() == Operation.DELETE) {
// 删除操作:删除缓存
redisTemplate.delete(cacheKey);
} else {
// 更新或插入:异步更新缓存
updateCacheInBackground(productId);
}
}
}
@Async
public void updateCacheInBackground(Long productId) {
try {
// 等待100ms,确保数据库事务提交
Thread.sleep(100);
// 查询最新数据
Product product = productRepository.findById(productId).orElse(null);
if (product != null) {
String cacheKey = "product:" + productId;
redisTemplate.opsForValue().set(cacheKey, product, 30, TimeUnit.MINUTES);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (Exception e) {
log.error("更新缓存失败,productId: {}", productId, e);
}
}
}
八、实战:电商商品系统完整缓存方案
零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。
8.1 多级缓存架构
用户请求 → Nginx本地缓存 → Redis集群缓存 → 数据库
↓ ↓
热点数据 全量数据
1ms 5ms
8.2 完整代码示例
@Service
@Slf4j
public class ProductCacheService {
// 本地缓存(Caffeine)
private Cache<Long, Product> localCache = Caffeine.newBuilder()
.maximumSize(1000) // 最多缓存1000个商品
.expireAfterWrite(1, TimeUnit.MINUTES) // 1分钟过期
.build();
@Autowired
private RedisTemplate<String, Product> redisTemplate;
@Autowired
private ProductRepository productRepository;
@Autowired
private BloomFilterService bloomFilterService;
/**
* 多级缓存查询
*/
public Product getProductWithMultiLevelCache(Long productId) {
// 1. 布隆过滤器判断是否存在
if (!bloomFilterService.mightContain("product:ids", String.valueOf(productId))) {
return null;
}
// 2. 查本地缓存
Product product = localCache.getIfPresent(productId);
if (product != null) {
log.debug("本地缓存命中,productId: {}", productId);
return product;
}
// 3. 查Redis缓存(防击穿)
product = getProductFromRedis(productId);
if (product != null && !isNullObject(product)) {
// 刷新本地缓存
localCache.put(productId, product);
return product;
}
// 4. 查数据库(防穿透、防雪崩)
product = getProductFromDatabase(productId);
// 5. 更新各级缓存
if (product != null) {
updateAllCaches(productId, product);
}
return product;
}
/**
* 从Redis获取商品(带互斥锁防击穿)
*/
private Product getProductFromRedis(Long productId) {
String cacheKey = "product:" + productId;
String lockKey = "lock:product:" + productId;
// 先查Redis
Product product = redisTemplate.opsForValue().get(cacheKey);
if (product != null) {
return product;
}
// 获取分布式锁
RLock lock = redissonClient.getLock(lockKey);
try {
boolean locked = lock.tryLock(50, 5000, TimeUnit.MILLISECONDS);
if (locked) {
// 再次检查缓存(Double Check)
product = redisTemplate.opsForValue().get(cacheKey);
if (product != null) {
return product;
}
// 查数据库
product = productRepository.findById(productId).orElse(null);
if (product != null) {
// 写入Redis,设置随机过期时间
int expireSeconds = 1800 + new Random().nextInt(600);
redisTemplate.opsForValue().set(
cacheKey, product, expireSeconds, TimeUnit.SECONDS
);
} else {
// 缓存空对象(防穿透)
Product nullProduct = createNullObject();
redisTemplate.opsForValue().set(
cacheKey, nullProduct, 300, TimeUnit.SECONDS
);
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
return product;
}
/**
* 更新所有缓存层级
*/
private void updateAllCaches(Long productId, Product product) {
// 1. 更新本地缓存
localCache.put(productId, product);
// 2. 更新Redis缓存(异步)
updateRedisCacheAsync(productId, product);
// 3. 更新布隆过滤器(如果新增商品)
bloomFilterService.add("product:ids", String.valueOf(productId));
}
@Async
public void updateRedisCacheAsync(Long productId, Product product) {
String cacheKey = "product:" + productId;
int expireSeconds = 1800 + new Random().nextInt(600);
redisTemplate.opsForValue().set(
cacheKey, product, expireSeconds, TimeUnit.SECONDS
);
}
}
九、缓存性能监控
零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。
9.1 监控关键指标
@Component
@Slf4j
public class CacheMonitor {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private MeterRegistry meterRegistry;
// 监控指标
private Counter cacheHitCounter;
private Counter cacheMissCounter;
private Timer cacheTimer;
@PostConstruct
public void init() {
// 初始化监控指标
cacheHitCounter = Counter.builder("cache.hits")
.description("缓存命中次数")
.register(meterRegistry);
cacheMissCounter = Counter.builder("cache.misses")
.description("缓存未命中次数")
.register(meterRegistry);
cacheTimer = Timer.builder("cache.latency")
.description("缓存操作延迟")
.register(meterRegistry);
}
/**
* 监控缓存查询
*/
public <T> T monitorCacheQuery(String cacheKey, Supplier<T> cacheQuery, Supplier<T> dbQuery) {
long start = System.currentTimeMillis();
try {
// 先查缓存
T result = cacheQuery.get();
if (result != null && !isNullObject(result)) {
// 缓存命中
cacheHitCounter.increment();
return result;
} else {
// 缓存未命中
cacheMissCounter.increment();
// 查数据库
result = dbQuery.get();
// 更新缓存
if (result != null) {
updateCache(cacheKey, result);
}
return result;
}
} finally {
long cost = System.currentTimeMillis() - start;
cacheTimer.record(cost, TimeUnit.MILLISECONDS);
// 记录日志
if (cost > 100) {
log.warn("缓存查询较慢,key: {}, 耗时: {}ms", cacheKey, cost);
}
}
}
/**
* 定期输出缓存统计报告
*/
@Scheduled(fixedRate = 60000) // 每分钟一次
public void printCacheStats() {
double hitRate = cacheHitCounter.count() /
(cacheHitCounter.count() + cacheMissCounter.count());
log.info("缓存统计 - 命中率: {:.2%}, 命中次数: {}, 未命中次数: {}",
hitRate, cacheHitCounter.count(), cacheMissCounter.count());
// 输出到监控系统
Metrics.gauge("cache.hit.rate", hitRate);
}
}
十、今日实战挑战
零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。
挑战题目: 设计一个"限时秒杀"系统的缓存方案,要求:
- 支持10万QPS
- 不能超卖
- 防止机器人刷单
- 保证公平性(先到先得)
设计要求:
- 画出系统架构图
- 写出关键代码
- 考虑容灾方案
- 设计监控指标
在评论区提交你的设计方案,我会选出3个最佳方案,送出《Redis设计与实现》纸质书!
明日预告:《SpringBoot+RabbitMQ:订单超时取消的完美实现》—— 消息队列的四种模式深度解析!
缓存工具包:关注公众号回复"缓存实战",获取完整的多级缓存实现代码和性能测试脚本!
公众号运营小贴士:
零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。
🎁 系列福利:
- 连续打卡7天,送《SpringBoot实战全家桶》课程
- 最佳方案作者,直推合作企业面试机会
- 随机抽10位幸运读者,送定制技术周边
👥 社群引导: "加入SpringBoot进阶群,获取完整学习路径+实战项目+面试指导"
🔥 明日剧透: "死信队列实现订单30分钟自动取消,RabbitMQ四种模式深度解析!"