旁路缓存(cache-aside pattern)是 redis 最常用的缓存策略,通过"先查缓存,后查数据库"的读写模式,显著提升系统读取性能
为什么需要缓存
在互联网应用中,数据库通常是系统性能的瓶颈。当面对高并发读取请求时,直接查询数据库会造成:
- 数据库压力过大:大量并发查询会导致数据库连接耗尽
- 响应延迟高:复杂查询可能需要数十甚至数百毫秒
- 扩展困难:通过增加数据库实例来提升性能成本高昂
缓存的出现正是为了解决这一问题。通过将热点数据存储在内存中,缓存能够提供微秒级的访问速度。
旁路缓存核心原理
旁路缓存(cache-aside pattern)是最经典的缓存应用模式,其核心思想是:应用层主动管理缓存,数据库为主,缓存为从。
读操作流程
public product getproduct(long id) {
// 1. 先查缓存
product cachedproduct = redis.get("product:" + id);
if (cachedproduct != null) {
// 缓存命中,直接返回
return cachedproduct;
}
// 2. 缓存未命中,查询数据库
product product = productmapper.findbyid(id);
if (product != null) {
// 3. 将数据写入缓存,设置过期时间
redis.set("product:" + id, product, 3600);
}
return product;
}读操作的关键步骤:
- 先从 redis 读取数据
- 缓存命中则直接返回
- 缓存未命中则查询数据库
- 将查询结果写入缓存并设置过期时间
写操作流程
public void updateproduct(product product) {
// 1. 先更新数据库
productmapper.update(product);
// 2. 再删除缓存
redis.del("product:" + product.getid());
}写操作采用先更新数据库,后删除缓存的策略,原因如下:
- 保证数据一致性:即使缓存删除失败,数据库数据仍是最新的
- 避免并发问题:相比更新缓存,删除缓存更能保证数据一致性
- 简化流程:不需要维护缓存与数据库的精确同步
缓存三大经典问题
1. 缓存穿透
问题描述:查询一个不存在的数据,由于缓存和数据库都没有,每次请求都会打到数据库。
危害:恶意用户可能利用此漏洞,大量请求不存在的数据导致数据库崩溃。
解决方案:
public product getproduct(long id) {
// 参数校验
if (id == null || id <= 0) {
return null;
}
// 1. 先查缓存
product cachedproduct = redis.get("product:" + id);
if (cachedproduct != null) {
return cachedproduct;
}
// 2. 缓存未命中,查询数据库
product product = productmapper.findbyid(id);
if (product == null) {
// 3. 缓存空值,防止穿透
redis.set("product:" + id, null, 60); // 短过期时间
return null;
}
// 4. 写入缓存
redis.set("product:" + id, product, 3600);
return product;
}额外防护措施:
// 使用布隆过滤器
private bloomfilter<long> bloomfilter;
public product getproduct(long id) {
// 布隆过滤器检查
if (!bloomfilter.mightcontain(id)) {
return null; // 一定不存在
}
// 正常查询流程
product product = getproductfromcacheordb(id);
return product;
}2. 缓存击穿
问题描述:某个热点数据过期的瞬间,大量并发请求同时发现缓存失效,全部涌入数据库查询。
解决方案:
方案一:互斥锁
private boolean lock = false;
public product getproduct(long id) {
product product = redis.get("product:" + id);
if (product != null) {
return product;
}
// 获取锁
if (trylock("lock:product:" + id)) {
try {
// double check
product = redis.get("product:" + id);
if (product != null) {
return product;
}
// 查询数据库
product = productmapper.findbyid(id);
redis.set("product:" + id, product, 3600);
} finally {
unlock("lock:product:" + id);
}
} else {
// 等待后重试
thread.sleep(100);
return getproduct(id);
}
return product;
}方案二:逻辑过期
public product getproduct(long id) {
// 1. 查缓存
product product = redis.get("product:" + id);
if (product == null) {
// 缓存为空,尝试获取锁重建缓存
if (trylock("lock:product:" + id)) {
product newproduct = productmapper.findbyid(id);
redis.set("product:" + id, newproduct, 3600);
unlock("lock:product:" + id);
return newproduct;
}
// 等待后重试
thread.sleep(100);
return getproduct(id);
}
// 2. 检查是否逻辑过期
if (islogicalexpired(product)) {
// 异步重建缓存,不阻塞请求
if (trylock("lock:product:" + id)) {
threadpool.execute(() -> {
product newproduct = productmapper.findbyid(id);
redis.set("product:" + id, newproduct, 3600);
unlock("lock:product:" + id);
});
}
}
return product;
}3. 缓存雪崩
问题描述:大量缓存数据在同一时间过期,导致大量请求同时打到数据库。
解决方案:
方案一:随机过期时间
// 设置过期时间添加随机值
int baseexpire = 3600;
int randomexpire = threadlocalrandom.current().nextint(300);
redis.set("product:" + id, product, baseexpire + randomexpire);方案二:多级缓存
public product getproduct(long id) {
// 1. 先查本地缓存
product product = localcache.get(id);
if (product != null) {
return product;
}
// 2. 查 redis
product = redis.get("product:" + id);
if (product != null) {
// 回填本地缓存
localcache.put(id, product, 300);
return product;
}
// 3. 查数据库
product = productmapper.findbyid(id);
redis.set("product:" + id, product, 3600);
return product;
}方案三:服务降级
public product getproduct(long id) {
try {
// 1. 查缓存
product product = redis.get("product:" + id);
if (product != null) {
return product;
}
// 2. 缓存未命中,降级处理
return getproductfrombackup(id);
} catch (exception e) {
// redis 异常,降级到数据库
log.error("redis error, fallback to db", e);
return productmapper.findbyid(id);
}
}数据一致性方案
延迟双删
public void updateproduct(product product) {
// 1. 删除缓存
redis.del("product:" + product.getid());
// 2. 更新数据库
productmapper.update(product);
// 3. 延迟删除缓存
threadpool.execute(() -> {
try {
thread.sleep(1000);
redis.del("product:" + product.getid());
} catch (interruptedexception e) {
thread.currentthread().interrupt();
}
});
}延迟双删的适用场景:写操作非常频繁,且对一致性要求较高。
订阅 binlog + canal
// canal 配置
@canalmessagelistener(topic = "product_db.product")
public void onmessage(canalentry.entry entry) {
canalentry.rowchange rowchange = canalentry.rowchange.parsefrom(entry.getstorevalue());
for (canalentry.rowdata rowdata : rowchange.getrowdataslist()) {
if (rowchange.geteventtype() == canalentry.eventtype.update) {
// 更新操作
for (column column : rowdata.getbeforecolumnslist()) {
if ("id".equals(column.getname())) {
long id = long.parselong(column.getvalue());
redis.del("product:" + id);
}
}
}
}
}优势:
- 异步更新,不影响主流程
- 保证最终一致性
- 适用于大型分布式系统
缓存策略最佳实践
缓存 key 设计
// 好的设计 string key = "product:info:" + categoryid + ":" + productid; string key = "user:profile:" + userid; string key = "order:summary:" + datestr; // 避免的设计 string key = "product_" + productid; // 缺少命名空间 string key = getcomplexkey(product); // 复杂计算 string key = "temp:" + system.currenttimemillis(); // 时效性数据
缓存 value 设计
// 序列化配置
@cacheable(value = "products", key = "#id", unless = "#result == null")
public product getproduct(long id) {
return productmapper.findbyid(id);
}
// json 序列化
@configuration
public class redisconfig {
@bean
public redistemplate<string, object> redistemplate(redisconnectionfactory factory) {
redistemplate<string, object> template = new redistemplate<>();
template.setconnectionfactory(factory);
jackson2jsonredisserializer<object> serializer = new jackson2jsonredisserializer<>(object.class);
objectmapper mapper = new objectmapper();
mapper.setvisibility(propertyaccessor.all, jsonautodetect.visibility.any);
mapper.activatedefaulttyping(mapper.getpolymorphictypevalidator());
serializer.setobjectmapper(mapper);
template.setkeyserializer(new stringredisserializer());
template.setvalueserializer(serializer);
template.sethashkeyserializer(new stringredisserializer());
template.sethashvalueserializer(serializer);
return template;
}
}过期时间策略
| 数据类型 | 过期时间 | 原因 |
|---|---|---|
| 热点商品 | 24 小时 | 数据相对稳定 |
| 用户会话 | 30 分钟 | 安全性考虑 |
| 排行榜数据 | 5 分钟 | 更新频繁 |
| 配置信息 | 1 小时 | 变更不频繁 |
| 计数器 | 不过期 | 需要持久化 |
容量规划
// 预估缓存容量
// 假设每秒 10000 次查询,缓存 10000 条数据
// 每条数据 1kb
// 需要的内存 = 10000 * 1kb = 10mb// 实际规划需要预留 20-30% 冗余
// 还需要考虑 redis 本身的内存开销
监控告警
# 监控指标
- alert: redishighmemoryusage
expr: redis_memory_used_bytes / redis_memory_max_bytes > 0.8
for: 5m
labels:
severity: warning
annotations:
summary: "redis 内存使用率过高"
- alert: redishighhitmissratio
expr: redis_keyspace_hits_total / (redis_keyspace_hits_total + redis_keyspace_misses_total) < 0.8
for: 5m
labels:
severity: warning
annotations:
summary: "redis 缓存命中率过低"性能优化技巧
批量操作
// 批量查询
public map<long, product> getproducts(list<long> ids) {
list<string> keys = ids.stream()
.map(id -> "product:" + id)
.collect(collectors.tolist());
list<product> products = redis.mget(keys);
map<long, product> result = new hashmap<>();
for (int i = 0; i < ids.size(); i++) {
if (products.get(i) != null) {
result.put(ids.get(i), products.get(i));
}
}
// 批量回补缓存
result.foreach((id, product) ->
redis.set("product:" + id, product, 3600)
);
return result;
}pipeline 批量写入
public void batchwriteproducts(list<product> products) {
redis.executepipelined((rediscallback<object>) connection -> {
for (product product : products) {
string key = "product:" + product.getid();
byte[] value = serializationutils.serialize(product);
connection.setex(key.getbytes(), 3600, value);
}
return null;
});
}缓存预热
@postconstruct
public void warmupcache() {
// 系统启动时预加载热点数据
log.info("start cache warmup...");
list<long> hotproductids = productservice.gethotproductids();
for (long id : hotproductids) {
product product = productmapper.findbyid(id);
redis.set("product:" + id, product, 3600);
}
log.info("cache warmup completed, {} products loaded", hotproductids.size());
}常见错误与规避
错误一:缓存与数据库双写不一致
// 错误写法:先更新缓存,再更新数据库
public void updateproduct(product product) {
redis.set("product:" + product.getid(), product); // 先更新缓存
productmapper.update(product); // 后更新数据库
// 并发时可能缓存是旧数据
}
// 正确写法:先删缓存,再更新数据库
public void updateproduct(product product) {
redis.del("product:" + product.getid()); // 先删缓存
productmapper.update(product); // 后更新数据库
}错误二:缓存频繁更新
// 错误写法:每次访问都更新缓存
public product getproduct(long id) {
product product = redis.get("product:" + id);
if (product == null) {
product = productmapper.findbyid(id);
}
// 每次都更新,浪费资源
redis.set("product:" + id, product, 3600);
return product;
}
// 正确写法:只在缓存不存在时更新
public product getproduct(long id) {
product product = redis.get("product:" + id);
if (product == null) {
product = productmapper.findbyid(id);
if (product != null) {
redis.set("product:" + id, product, 3600);
}
}
return product;
}错误三:大对象缓存
// 错误写法:缓存整个列表
public list<product> getallproducts() {
list<product> products = redis.get("all_products");
if (products == null) {
products = productmapper.findall();
redis.set("all_products", products, 300);
}
return products;
}
// 正确写法:分页缓存或使用压缩
public list<product> getproducts(int page, int size) {
string key = "products:" + page + ":" + size;
return redis.get(key);
}总结
旁路缓存是提升系统读取性能的利器,但在实际应用中需要注意:
- 合理设计:根据业务特点选择合适的缓存策略
- 一致性保障:根据一致性要求选择延迟双删或 binlog 方案
- 容错机制:做好缓存穿透、击穿、雪崩的防护
- 监控告警:实时监控缓存命中率、内存使用等关键指标
- 预热与降级:系统启动时预热缓存,异常时做好降级
正确使用旁路缓存,能够将系统性能提升一个数量级,是后端开发必备的核心技能。
到此这篇关于redis 旁路缓存深度解析的文章就介绍到这了,更多相关redis 旁路缓存内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论