读写锁应用到缓存更新
一、读取缓存流程
我们知道,Redis是一个Nosql数据库,由于其数据都放在内存中,常常用来做缓存。Redis用作缓存,肯定要和数据库打交道。当然Redis的应用场景还有很多,不光只用作缓存。
在读取缓存方面,都是按照下图的流程来进行业务操作。
但兄弟们有没有想过如果数据库中数据修改了,那么缓存的更新策略是什么样呢?我们现在就来讨论这个问题。首先我们看看模拟查询缓存代码。
二、模拟查询缓存代码
定义一个Map集合作为缓存,在实际生产中使用Redis来做缓存,实际上Redis的底层结构也是Map集合。
class GenericDaoCached extends GenericDao {
private GenericDao dao = new GenericDao();
private Map<SqlPair, Object> map = new HashMap<>(); //定义Map集合
@Override
public <T> T queryOne(Class<T> beanClass, String sql, Object... args) {
// 先从缓存中找,找到直接返回
SqlPair key = new SqlPair(sql, args);
T value = (T)map.get(key);
if(value!=null){
return value;
}
//缓存中没有,查询数据库,然后将查询结果放到缓存中
dao.queryOne(beanClass, sql, args);
map.put(key,value);
return value;
}
@Override
public int update(String sql,Object... args){
//更新时,先清缓存
map.clear();
//再更新数据库
return dao.update(sql,args);
}
}
首先分析一下,上述代码可能会出现什么问题?
-
集合是HashMap,它是线程不安全的,在多线程访问下就有可能丢失数据(当多线程同时put值的时候,若发生hash碰撞,可能多个元素都落在链表的头部,从而造成元素覆盖)
-
清空缓存和更新数据库这两个操作有可能导致查询的值和数据库中的值不一致
三、常见的缓存更新策略
首先,我们思考一下更新时,是先清除缓存还是先更新数据库?
先清缓存,再更新数据库
结果:造成查询的值和数据库中的值不一致
先更新数据库,再清除缓存
结果:造成A线程首次查询和后续查询得到不一致的结果,首次查询得到 x=1,后续查询发现已经清空了缓存,需要去数据库中查得 x=2
四、读写锁应用到缓存更新策略
class GenericCachedDao<T> {
// HashMap 作为缓存非线程安全, 需要保护
HashMap<SqlPair, T> map = new HashMap<>();
//读写锁
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public T queryOne(Class<T> beanClass, String sql, Object... params) {
SqlPair key = new SqlPair(sql, params);
// 加读锁, 防止其它线程对缓存更改
lock.readLock().lock();
try {
T value = map.get(key);
if (value != null) {
return value;
}
} finally {
lock.readLock().unlock();
}
// 加写锁, 防止其它线程对缓存读取和更改
lock.writeLock().lock();
try {
// get 方法上面部分是可能多个线程进来的, 可能已经向缓存填充了数据
// 为防止重复查询数据库, 再次验证
T value = map.get(key);
if (value == null) {
// 如果没有, 查询数据库
value = genericDao.queryOne(beanClass, sql, params);
map.put(key, value);
}
return value;
} finally {
lock.writeLock().unlock();
}
}
public int update(String sql, Object... params) {
SqlPair key = new SqlPair(sql, params);
// 加写锁, 防止其它线程对缓存读取和更改
lock.writeLock().lock();
try {
int rows = genericDao.update(sql, params);
map.clear();
return rows;
} finally {
lock.writeLock().unlock();
}
}
当然,实现缓存更新策略还有很多方法,我们下回再述. . . . . .