分布式缓存问题与解决

文章详细阐述了在分布式缓存架构中遇到的一系列问题,包括数据冷热分离、缓存击穿、缓存穿透、缓存重建和双写不一致,以及缓存雪崩等。为了解决这些问题,提出了如设置随机超时时间、使用分布式锁、存储空值标记、读写锁优化和多级缓存策略等解决方案,以确保系统的稳定性和性能。
摘要由CSDN通过智能技术生成

在分布式缓存架构中,随着并发量的提高,一致性和可用性会出现各种各样的问题

1、为了防止冷门数据占据内存,加超时时间,让缓存尽量只保留热门数据---- 数据冷热分离

2、若很多缓存被设置了同一个超时时间,在同一时刻超时被清理,此时若大量请求访问这些过期的key,将会因为没有缓存而直接访问数据库,对数据库造成压力。------被称为缓存失效、缓存击穿

解决:超时时间设置一小段的随机时间,防止同一时刻过期

3、若管理端删除了某个数据,前端还停留在被删数据页面,此时前端请求了已经不存在的数据,缓存查找没有,会访问数据库,前端若一直请求,会持续对数据库造成压力。 或是黑客行为也会造成这种情况。------缓存穿透

解决:当经过数据库查询该数据不存在时, 在缓存中存入一个标记,表明该数据不存在,并设定一个较短的过期时间,防止占据内存。

4、若大量并发瞬间访问了一个冷门数据(无缓存的),请求发现无缓存,会进行缓存重建,大量请求进行缓存重建,对数据库压力较大。------------缓存重建

解决:增加分布式锁,只允许一个线程重建缓存,如同多线程编程中的单例创建的双重检测

5、在update操作时,需要更新数据库,更新缓存两个操作,这两个操作是非原子性的,在两个操作之间,数据库与缓存数据是不一致的,此时的不一致时间还不是很长。 如果在更新数据库时,进行了缓存重建,缓存重建线程拿到了旧数据,并且覆盖了缓存,将会造成一直不一致。--------双写不一致

解决:删缓存更新,并不能解决根本问题,需要在读写操作之间加锁。

加锁后会变成串行执行,效率很差,可以用读写锁进行优化, 缓存重建用读锁,更新数据库用写锁,当有更新数据库时,会变成互斥锁

写数据库
写入中
写缓存新值
缓存重建
读数据库
时间片切换
写缓存旧值

6、当热点数据所在的redis节点不可访问时,会造成连锁阻塞,从而影响整个系统 ------缓存雪崩

解决:使用多级缓存,redis不可访问时,暂时使用本地缓存。或是业务端增加熔断降级

示例代码

public class CurdService {

    static class Bag{
        public Bag(Long id, String name) {
            this.id = id;
            this.name = name;
        }

        public Long id;
        public String name;
    }
    static class BagDao{
        public void insert(Bag bag){

        }
        public void update(Bag bag){

        }
        public void delete(Long id){

        }
        public Bag get(Long id){
            return new Bag(id,"name");
        }
    }

    private BagDao bagDao;
    private RedissonClient redissonClient;

    private Map<String, Object> localCache = new HashMap<>();

    public void save(Bag bag){
        bagDao.insert(bag);

        RMap<String, Object> map = redissonClient.getMap("bag:"+bag.id);
        addCache(map, bag);
    }

    public void update(Bag bag){
        //⑤ 双写一致
        //如果全部都在读缓存,也就不会出现一致性问题;
        //在mysql读与写操作时,因为是并发操作,此时数据会有短暂的不一致,
        // 若此时进行了缓存重建: update 更新mysql 写缓存; 缓存重建 读mysql 写缓存
        //因为读写的不一致,从而影响到缓存更新的不一致
        //因此,需要在更新数据和重建缓存的地方加锁, 防止数据不一致
        //加锁后会变成串行执行,效率很差
        //由于缓存重建和写库同时进行的概率很小,可以用读写锁进行优化, 缓存重建用读锁,更新数据库用写锁,当有更新数据库时,会变成互斥锁。
        //通过读写锁实现
        RReadWriteLock rwLock = redissonClient.getReadWriteLock("rwlock:bag:"+bag.id);
        RLock lock = rwLock.writeLock();
        lock.lock(10, TimeUnit.SECONDS);
        try {
            bagDao.update(bag);

            RMap<String, Object> map = redissonClient.getMap("bag:" + bag.id);
            addCache(map, bag);
        }finally {
            lock.unlock();
        }
    }

    public void delete(Long id){
        bagDao.delete(id);

        redissonClient.getMap("bag:"+id).delete();
        localCache.remove(id);
    }

    public Bag get(Long id){

        //⑥ 当热点数据所在的redis节点不可访问时,会造成连锁阻塞,从而影响整个系统 ------缓存雪崩
        //解决:业务端增加熔断降级
        //缓存这边可使用多级缓存, 在jvm进程中缓存少量热点数据
        //todo ** 使用本地缓存,在集群服务中会存在不一致的情况,需要通过mq  redis stream 进行数据同步
        Bag localBag = (Bag)localCache.get(id.toString());
        if(localBag!=null){
            return localBag;
        }

        RMap<String, Object> map = redissonClient.getMap("bag:"+id);
        if(!map.isEmpty()){
                Bag bag = new Bag((Long)map.get("id"), (String) map.get("name"));
                //① 延长热门数据的过期时间
                map.expire(100L, TimeUnit.MINUTES);
                return bag;
        }
        //==========未命中,重建缓存===========

        Bag bag = null;

        //④ 缓存重建, 防止大量请求同时重建缓存,造成数据库压力
        //增加分布式锁, 只允许一个线程进行缓存重建
        //双重检测
        RLock lock = redissonClient.getLock("lock:bag:"+id);
        lock.lock(10, TimeUnit.SECONDS);
        try {
            //④ 再次判断缓存是否已重建
            if(!map.isEmpty()) {
                addCache(map, bag);
            }

            //⑤ 为了提高一致性, 增加双写一致
            //加读锁
            RReadWriteLock rwLock = redissonClient.getReadWriteLock("rwlock:bag:"+id);
            RLock rlock = rwLock.readLock();
            try{
                rlock.lock(10, TimeUnit.SECONDS);
                bag = bagDao.get(id);
            }finally {
                rlock.unlock();
            }

            //③ 访问了不存在的数据,缓存未命中, 继而访问数据库 ----- 缓存穿透
            //缓存增加空值, 使用一个较短的过期时间,防止长时间占用内存
            if(bag == null){
                map.put("id", null);
                map.put("name", null);
                map.expire(100L, TimeUnit.SECONDS);
            }else {

            }

        }finally {
            lock.unlock();
        }
        return bag;
    }

    private void addCache(RMap<String, Object> map, Bag bag){

        map.put("id", bag.id);
        map.put("name", bag.name);

        //① 加超时,节约内存,防止冷门数据占用空间
//        map.expire(100L, TimeUnit.MINUTES);
        //② 防止同一时期过期,缓存命中失败,对数据库造成压力  ----- 缓存失效、缓存击穿
        map.expire(100L + new Random().nextInt(10), TimeUnit.MINUTES);

        //⑥ 多级缓存本地更新和广播
        if(true){ //热点数据判断
            //放入本地缓存, 并加上过期时间
            localCache.put(bag.id.toString(), bag);
            //mq.push(bag);
        }

    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值