用读写锁实现一个缓存系统

一、用读写锁设计缓存系统

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;
 
/**
 * 用读写锁实现的一个缓存系统,读的时候可以并发执行,当缓存中没有数据时,要到数据库中查询数据
 * 此时只能写数据,不能读数据。当完数据之后,又可以并发地读取数据。
 * 这样做的话,可以提高 系统的效率
 * @author  zc
 *
 */
public class MyCacheSystem {
 
    // 定义一个map用来存放要缓存起来的数据
    Map<String, Object> cache = new HashMap<String, Object>();
 
    private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
 
    public static void main(String[] args) {
 
    }
 
    //该方法中,读数据可以并发地读取,写数据与读数据,写数据与写数据之间不能并发地运行
    public Object getData(String key) {
        //刚进来的时候,上一把读锁
        rwl.readLock().lock();
        Object obj = null;
        try {
            obj = cache.get(key);
            if (obj == null) {
                //如果数据为空,则需要到数据库中查询数据,所以这时候把读锁释放掉,上一把写锁,不能同时写数据
                //在上写锁之前,首先要把读锁释放掉
                rwl.readLock().unlock();
                rwl.writeLock().lock();
                // 查询数据库的代码
                try {
                     
                    //必须重新检查obj是否为空,因为这时候,另外一个线程可能会获得写锁,从而让obj有值
                    if (obj == null) {
                        obj = "查询数据库得到的数据";
                    }
                } finally {
                    rwl.writeLock().unlock();
                }
                //因为前面释放了写锁,所以这里要把读锁重新锁上
                rwl.readLock().lock();
            }
        } finally {
            rwl.readLock().unlock();
        }
        return obj;
    }
}

二、缓存系统的设计考虑问题

1. 缓存穿透的解决方法

缓存穿透是指查询一个根本不存在的数据,缓存层和存储层都不会命中,但是出于容错的考虑,如果从存储层查不到数据则不写入缓存层。缓存穿透将导致不存在的数据每次请求都要到存储层去查询,失去了缓存保护后端存储的意义。
缓存穿透问题可能会使后端存储负载加大,由于很多后端存储不具备高并发性,甚至可能造成后端存储宕掉。通常可以在程序中分别统计总调用数、缓存层命中数、存储层命中数,如果发现大量存储层空命中,可能就是出现了缓存穿透问题。
造成缓存穿透的基本有两个。第一,业务自身代码或者数据出现问题,第二,一些恶意攻击、爬虫等造成大量空命中,下面我们来看一下如何解决缓存穿透问题。
缓存穿透的解决方法:1)缓存空对象;2)布隆过滤器拦截器。

2. 缓存雪崩问题解决

由于缓存层承载着大量请求,有效的保护了存储层,但是如果缓存层由于某些原因整体不能提供服务,于是所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会挂掉的情况。 缓存雪崩的英文原意是 stampeding herd(奔逃的野牛),指的是缓存层宕掉后,流量会像奔逃的野牛一样,打向后端存储。

预防和解决缓存雪崩问题,可以从以下三个方面进行着手。
1)保证缓存层服务高可用性。
和飞机都有多个引擎一样,如果缓存层设计成高可用的,即使个别节点、个别机器、甚至是机房宕掉,依然可以提供服务,例如前面介绍过的 Redis Sentinel 和 Redis Cluster 都实现了高可用。
2)依赖隔离组件为后端限流并降级。
无论是缓存层还是存储层都会有出错的概率,可以将它们视同为资源。作为并发量较大的系统,假如有一个资源不可用,可能会造成线程全部 hang 在这个资源上,造成整个系统不可用。降级在高并发系统中是非常正常的:比如推荐服务中,如果个性化推荐服务不可用,可以降级补充热点数据,不至于造成前端页面是开天窗。

3)提前演练。在项目上线前,演练缓存层宕掉后,应用以及后端的负载情况以及可能出现的问题,在此基础上做一些预案设定。

3. 缓存热点key重建优化

开发人员使用缓存 + 过期时间的策略既可以加速数据读写,又保证数据的定期更新,这种模式基本能够满足绝大部分需求。但是有两个问题如果同时出现,可能就会对应用造成致命的危害:
-> 当前 key 是一个热点 key( 例如一个热门的娱乐新闻),并发量非常大。
-> 重建缓存不能在短时间完成,可能是一个复杂计算,例如复杂的 SQL、多次 IO、多个依赖等。

猜你喜欢

转载自blog.csdn.net/u012017783/article/details/82779124