如何使用读写锁设计一个简单实用的缓存系统

开发中我们经常使用到缓存,第一次请求的时候从数据库中获取数据,然后保存到缓存中,这样下次访问的时候就可以直接从缓存中返回数据了,代码如下

/**
 * 

缓存设计

* * @Author donny * @Date 2022/9/1 */ public class CachedUtil { /** * 缓存 */ public static Map MAP_CACHE = new HashMap<>(); /** * 查询数据 * @param key * @return */ public Object getData(String key) { Object value = MAP_CACHE.get(key); if (value == null) { value = loadDB(key); MAP_CACHE.put(key, value); } return value; } /** * 模拟从数据库获取数据 * * @param key * @return */ private Object loadDB(String key) { return new Object(); } }

但是如果某个热点key,在不停地扛着大并发,在这个key失效的瞬间,持续的大并发请求就会击破缓存,直接请求到数据库,造成缓存击穿:

public Object getData(String key) {
        Object value = MAP_CACHE.get(key);
        // 大并发的请求某个key,如果该key在缓存中不存在,就会直接请求到数据库,造成缓存击穿
        if (value == null) {
            value = loadDB(key);
            MAP_CACHE.put(key, value);
        }
        return value;
    }

很自然的我们想到加一个锁来解决这个问题:

public static Lock lock = new ReentrantLock();
public Object getData(String key) {
        lock.lock();
        try {
            Object value = MAP_CACHE.get(key);
            if (value == null) {
                value = loadDB(key);
                MAP_CACHE.put(key, value);
            }
            return value;
        } finally {
            lock.unlock();
        }
    }

但是一旦加锁后所有的访问都变成了串行访问,为了提高效率,我们决定改成读写锁来实现:

public static ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
 public Object getData(String key) {
   			// 使用读锁
        readWriteLock.readLock().lock();
        try {
            Object value = MAP_CACHE.get(key);
            if (value == null) {
                // 如果缓存中不存在则取消读锁,改用写锁
                readWriteLock.readLock().unlock();
                readWriteLock.writeLock().lock();
                try {
                    value = loadDB(key);
                    MAP_CACHE.put(key, value);
                } finally {
                    // 通过在释放写锁之前获取读锁进行降级
                    readWriteLock.readLock().lock();
                    // 解锁写锁,此时仍然持有读锁
                    readWriteLock.writeLock().unlock();
                }
            }
            return value;
        } finally {
            readWriteLock.readLock().unlock();
        }
    }

但是这样写的话还是存在一个小问题:

				       // 多个线程排队获取写锁
                readWriteLock.writeLock().lock();
                try {
                    // 可能前面获取到写锁的线程已经将值写入了缓存,所以此处还需要再次判断下value是否为null
                    value = loadDB(key);
                    MAP_CACHE.put(key, value);
                } 

最终改写后的代码:

 public Object getData(String key) {
        // 使用读锁
        readWriteLock.readLock().lock();
        try {
            Object value = MAP_CACHE.get(key);
            if (value == null) {
                // 如果缓存中不存在则取消读锁,改用写锁
                readWriteLock.readLock().unlock();
                // 多个线程排队获取写锁
                readWriteLock.writeLock().lock();
                try {
                    // 可能前面获取到写锁的线程已经将值写入了缓存,所以需要再次判断value是否为null
                    value = MAP_CACHE.get(key);
                    if (value == null) {
                        value = loadDB(key);
                        MAP_CACHE.put(key, value);
                    }
                } finally {
                    // 通过在释放写锁之前获取读锁进行降级
                    readWriteLock.readLock().lock();
                    // 解锁写锁,此时仍然持有读锁
                    readWriteLock.writeLock().unlock();
                }
            }
            return value;
        } finally {
            readWriteLock.readLock().unlock();
        }
    }

总结:

  1. 利用了ReentrantReadWriteLock的读读共享、读写互斥、写写互斥
  2. 获取到写锁的时候还需要再次从缓存中查询下数据,防止其他线程已经将数据写入了缓存。这其实跟dcl问题中第二个判空不能去除本质是一样的。
发表评论
留言与评论(共有 0 条评论) “”
   
验证码:

相关文章

推荐文章