订单缓存实践
最近在做订单缓存查询相关需求,记录下该过程中缓存查询考虑的几个问题以及处理方案。
缓存穿透
实际场景中使用缓存都是先去缓存中查询,如果缓存没有命中,在去查询数据库并将结果缓存。如果查询一个在系统中根本就不存在的数据,就会造成每次请求都会穿透缓存去查询数据库。如果出现大量的缓存穿透(或者恶意攻击),就会对数据库造成比较大的压力。
处理方案
对于数据库中不存在的数据,存储特定的值表示数据不存在。在发生insert之后将缓存中对应数据移除,避免在数据生成之后缓存中查询还是NULL。
1 public Order findById(Long orderId) {
2 // 从数据库查询
3 Order order = getOrderFromDB(orderId);
4 if (order == null) {
5 // 订单不存在时候填入特定"NULL"表示订单不存在
6 putOrderToCache(orderId, "NULL");
7 } else {
8 putOrderToCache(orderId, order);
9 }
10 return order;
11 }
缓存并发
在查询同一并发量较大情况下,如果该订单缓存失效,就会造成这一瞬间所有的订单查询请求都会访问到数据库,造成数据库压力增大。
处理方案
并发查询同一订单,缓存中不存在,加锁查询数据库,其余请求等待获取锁或者获取超时返回数据或者查询数据库。
public Order findById(Long orderId) {
// 从缓存中获取
Result result = getOrderFromCache(orderId);
Order order = result.getOrder();
if (result.isEmpty()) {
// 从DB获取数据
order = getOrderFromDB(orderId);
// 写入缓存
putOrderToCache(orderId, order);
}
return order;
}
上面这种写法,一般是没什么问题,在并发量大时候会造成所有请求都查询到数据库。并发100个请求查询同一个订单,当缓存没数据时100请求都会到DB。为了处理这种情况,解决的方式就是加锁。
public Order findById(Long orderId) {
// 从缓存中获取
Result result = getOrderFromCache(orderId);
Order order = result.getOrder();
if (result.isEmpty()) {
try {
if (lock.tryLock(1)) {
// 从DB获取数据
order = getOrderFromDB(orderId);
// 写入缓存
putOrderToCache(orderId, order);
} else {
Thread.sleep(10);//10毫秒根据业务场景自定义
return findById(orderId);
}
} catch (Exception e) {
//
} finally {
lock.unlock();
}
}
return order;
}
尝试获取一次锁,并发情况下,获取不到锁就重新走一遍流程(先查缓存,在查DB),加锁时间不要设置太长,避免过多的线程在等待。采用这种方式,当有100个请求并发查询时候,一个线程拿到锁查询DB,剩余99个线程在等待10毫秒重试,比如查询DB线程获取到数据需要耗时5毫秒,10毫秒之后99个线程发现缓存中有值,直接从缓存中取值,耗时10毫秒。
缓存失效
为了避免缓存中的数据越来越多或者缓存一些很少用到或者根本不会使用到数据,通常都会在订单缓存中加入过期时间,比如几分钟。当某些情况下,会出现多个订单的过期时间是一样的,即多个订单缓存数据同时失效。
处理方案
对于每个订单的缓存失效时间都不一样,失效时间都是一个范围内随机值,可以在一定程度上减少缓存同时失效