如何用CodeQL检查缓存击穿问题
0x01 背景简介
- CodeQL
https://blog.csdn.net/D_V_K_/article/details/107935368
- 缓存击穿
缓存击穿是指缓存中没有但数据库中有的数据,当并发用户在一瞬间断崖式增长,没有从缓存读到数据,会造成同时去数据库取数据,导致DB压力过大甚至崩溃。
本文以 Java为主要语言,主要讨论使用CodeQL定义业务代码中缓存击穿的场景。
0x02 模式分析
为寻找缓存击穿的业务场景,首先对该场景进行分析。为避免缓存击穿问题,常见的解决方案是加入互斥锁,伪代码如下:
public Object getData() throws Exceptions {
// 读取缓存
Object value = getCache();
// 没读到缓存
if (value == null) {
// 加锁,准备读取db
locked();
// 二次判断是否存在缓存, 存在说明有其他线程完成了db操作。
if (getCache()) {
return getCache();
}
// 查询数据库,并写到缓存,让其他线程可以直接走缓存
value = getDBData();
}
}
// 释放锁
unlocked();
return value;
}
而大多数业务方在操作时可能会没有进行加锁操作或是仅进行了一次缓存判断的操作。
因此,我们要通过CodeQL定位的代码应该是这种模式:
public Object getData() throws Exceptions {
// 读取缓存
Object value = getCache();
// 没读到缓存
if (value == null) {
// 加锁,准备读取db
locked();
// Bad: 没有查询是否有其他线程完成了任务,直接查询了db
value = getDBData();
}
}
// 释放锁
unlocked();
return value;
}
0x03 定义模型
sink
通过分析,我们将所有涉及到db操作的函数定义为我们要检查的sink函数。那么如何去查找所有操作db动作的函数呢。这里主要通过两个思路:
- 通过项目结构来定义:公司级别的项目多数都有着特定的规范,当我们查询发现某些方法处于固定的目录(如:Mapper),可以粗略的定义为是操作db的方法函数。
- 通过父依赖判断:是否使用了常见ORM框架或db操作的SDK如:Jdbc、Mybatis、Hiberante等。
这里因为公司大部分使用的为Mybatis,且命名规范基本包含Mapper,因此定义QL规则如下:
// 列出所有db操作的方法
class DBMethod extends Method {
DBMethod() {
exists(Interface i | i.getLocation().toString().matches("%Mapper%") | this = i.getAMethod())
}
}
source
对于缓存穿透的场景,我们需要查找的db操作一定是使用了缓存的地方(不考虑应该增加缓存操作的db方法)。所以我们的source定义为:所有调用缓存相关的方法的位置,作为我们搜索的起点。
常用的缓存类有
com.google.common.cache
redis.clients.jedis
org.springframework.data.redis
....
isSink
如何判断从调用缓存到操作db之间是存在问题的,主要通过是否进行了锁操作来进行check。
常用的锁操作有:
synchronized
ReadLock
WriteLock
ReadWriteLock
ReentrantLock
ReentrantReadWriteLock
或是通过检测父类是否继承于
java.util.concurrent.locks.Lock
xxx.lock()
lockInterruptibly()
tryLock(long time, TimeUnit unit)
0x04 规则效果
0x05 不足
新人小白自己学习代码审计,如果有任何错误或是更好的方案,欢迎各位大佬指正和改进。