Redis 笔记三

时间:2024-01-25 14:30:31

概览:

1.中小公司Redis缓存架构以及线上问题分析

2.大厂线上大规模商品缓存数据冷热分离实战

3.实战解决大规模缓存击穿导致线上数据库压力暴增

4.黑客攻击导致缓存穿透线上数据库宕机bug

5.一行代码解决线上缓存穿透问题

6.一次大V直播带货导致线上商品系统崩溃原因分析

7.突发性热点缓存重建导致系统压力暴增问题分析

8.基于DCL机制解决热点缓存并发重建问题实战

9.Redis分布式锁解决缓存和数据库双写不一致问题

10.大促压力暴增导致分布式锁串行争用问题优化实战

11.一次微博明星热点事件导致系统崩溃原因分析

12.利用多级缓存架构解决Redis线上集群缓存雪崩问题

13.基于分布式实时计算机制实现JVM热点缓存存储

14.基于Sentine限流降级熔断机制解决Redis缓存雪崩问题

1.基础的Redis缓存架构

秒杀场景种关于Redis缓存使用中的一些问题及解决。

常规操作:

  •         首先在插入对象的同时,要在Redis中缓存,以减轻数据库的访问压力
  •         当数据发生更新时,也要同步更新Redis里的数据。此时有两种方式:(1)删除,重新插入(2)修改原值。

改进一:

  •         一个数据放缓存里面时,要考虑数据量,如果数据量过大最好设置一个缓存时间。防止其一直占用内存空间,对Redis来说也有压力。
package com.example.service;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.example.dao.ProductDao;
import com.example.domin.Product;
import com.example.util.RedisKeyPrefixConst;
import com.example.util.RedisUtil;
import jodd.io.StreamUtil;
import org.redisson.Redisson;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

public class ProductService {

    @Autowired
    private RedisUtil redisUtil;

    @Autowired
    private ProductDao productDao;

    @Autowired
    private Redisson redisson;
    public static final Integer PRODUCT_CACHE_TIMEOUT = 60*60*24;

    @Transactional
    public Product create(){
        Product productResult = productDao.create();
        redisUtil.set(RedisKeyPrefixConst.PRODUCT_CACHE+productResult.getId(), JSON.toJSONString(productResult));
        return  productResult;
    }

    @Transactional
    public Product update(Product product){
        Product productResult = productDao.update(product);
        redisUtil.set(RedisKeyPrefixConst.PRODUCT_CACHE+productResult.getId(), JSON.toJSONString(productResult));
        return  productResult;
    }

    public Product get(Long productId){
        Product product=null;
        String producyCacheKey = RedisKeyPrefixConst.PRODUCT_CACHE+productId;
        
        String productStr= redisUtil.get(producyCacheKey);
        if(!StringUtils.isEmpty(productStr)){
            product=JSONObject.parseObject(productStr,Product.class);
            return product;
        }
        return  product;
    }
}

当前实现存在的问题:在高并发场景下,redis可能发生的缓存击穿,缓存穿透、缓存雪崩等问题。

2.Redis框架高并发下问题分析

(1)缓存击穿

场景介绍

        比如有一批商品数据信息需要更新,通过批量导入来更新。这时Redis之前缓存的数据同时大量失效。这时请求进入数据库,数据库面临大访问量的压力,甚至可能崩掉。这种情况叫做缓存击穿。

        即大量访问没有在redis内获取到信息,同时涌入数据库,导致数据库访问压力骤增的现象。

解决方案:超时时间相同,所以可能存在同时失效问题。对于由于缓存超时时间同时失效导致的缓存击穿问题,可以通过给超时时间加所随机数,来防止同一时间缓存失效问题。

public Product get(Long productId){
        Product product=null;
        String producyCacheKey = RedisKeyPrefixConst.PRODUCT_CACHE+productId;

        String productStr= redisUtil.get(producyCacheKey);
        if(!StringUtils.isEmpty(productStr)){
            product=JSONObject.parseObject(productStr,Product.class);
            return product;
        }

        product = productDao.findProductById(productId);
        if(product!=null){
            redisUtil.set(producyCacheKey,JSON.toJSONString(product),PRODUCT_CACHE_TIMEOUT, TimeUnit.SECONDS);
        }
        return  product;
    }

    private Integer getProductCacheTimeout(){
        return PRODUCT_CACHE_TIMEOUT+new Random().nextInt(30)*60;
    }

(2)缓存穿透

场景介绍:

        后天人员把一个商品数据删除了,大量的访问查redis没查到,又查数据库也没查到。这就导致大量的访问即给到Redis,又给到数据库。Redis和数据库都面临访问压力骤增甚至瘫痪的问题。这种场景称为:缓存穿透。

        即大量请求访问redis无法获取数据,转而访问数据库,造成redis和数据库都面临访问压力的情况,称为缓存穿透。(redis和数据库都面临大数据量查询压力,把缓存和数据库全部打穿,打崩

解决方案:当查到数据库内没有这个数据时,在redis设置一个null值,下次就不查数据库了,来解决数据库的访问压力问题。

    public Product get(Long productId){
        Product product=null;
        String producyCacheKey = RedisKeyPrefixConst.PRODUCT_CACHE+productId;

        String productStr= redisUtil.get(producyCacheKey);
        if(!StringUtils.isEmpty(productStr)){
            product=JSONObject.parseObject(productStr,Product.class);
            return product;
        }

        product = productDao.findProductById(productId);
        if(product!=null){
            redisUtil.set(producyCacheKey,JSON.toJSONString(product),PRODUCT_CACHE_TIMEOUT, TimeUnit.SECONDS);
        }else{
            redisUtil.set(producyCacheKey,EMPTY_CACHE);
        }
        return  product;
    }

(3)突发性热点重构

场景介绍:        

        比如一个大V带货一个冷门商品(商品信息不在redis内),导致这个冷门商品突然变成热门商品(突发性热点|大量访问请求),导致页面崩掉(大量请求同一时间访问redis,没有获得信息,又同时访问数据库,压力给到数据库; 如果数据库能够支撑,此时又同时修改redis缓存,导致redis访问压力)。称之为突发性热点重构问题,通常导致访问压力暴增。

即:不在redis内的数据,突然受到大量访问。先大量访问压力给到数据库,后大量缓存重构请求同时执行,压力给到redis的现象,成为突发性热点重构问题

解决方案:

1.提前缓存,只能解决已知的突发性热点重构问题,对于未知的突发性热点重构来说没用。

2.双重检测锁-单力模式,当大量请求访问数据库加锁,让一个请求取查数据库,并将值重构到redis内,其余请求在redis内获取数据,不在进入数据库。就不会发生上述大量请求同时访问数据库、同时设置redis的问题。

TipS: 重复代码随时重构,保证代码简洁性,保持良好编码习惯。

DCL:双重校验锁

     public Product get(Long productId){
        Product product=null;
        String producyCacheKey = RedisKeyPrefixConst.PRODUCT_CACHE+productId;
        //DCL:DCL:double-checked locking 中文一般译为“双重检查锁
        synchronized (this){
            product =getProductFormCVache(producyCacheKey);
            if(product!=null){
                return product;
            }

            product = productDao.findProductById(productId);
            if(product!=null){
                redisUtil.set(producyCacheKey,JSON.toJSONString(product),PRODUCT_CACHE_TIMEOUT, TimeUnit.SECONDS);
            }else{
                redisUtil.set(producyCacheKey,EMPTY_CACHE);
            }
        }
        return  product;
    }

    private Integer getProductCacheTimeout(){
        return PRODUCT_CACHE_TIMEOUT+new Random().nextInt(30)*60;
    }

    private Product getProductFormCVache(String productCacheKey){
        Product product=null;
        String productStr= redisUtil.get(productCacheKey);
        if(!StringUtils.isEmpty(productStr)){
            if(EMPTY_CACHE.equals(productStr)){
                return null;
            }
            product=JSON.parseObject(productStr,Product.class);
        }
        return product;
    }

改进:加锁可能导致的一些问题:当应对高并发场景时:

(1)当前使用的锁是synchronized,它是单机进程级别的锁

解决方式:分布式锁,在集群环境下也可以保证功能实现

(2)应该锁加到具体资源,不然访问商品101加锁时,1066也得排序,锁粒度要尽可能的小,不然会影响性能。

解决方式:全局对象池:用对象加锁,要维护对象池——麻烦

分布式锁:解决上述问题

SETNX:

        setnx key value

        将key的值设置为value, 当且仅当key不存在

        若给定key已经存在,则SETNX不做任何动作。

        SETNX是set if not esists的简写

可用版本: >=1.0.0

  • 对于锁加一个前缀有利于后续维护工作,其次常量长一点没关系,信息更清楚。
  • try catch finally要加锁,还要释放
    public Product get(Long productId){
        Product product=null;
        String producyCacheKey = RedisKeyPrefixConst.PRODUCT_CACHE+productId;
        //DCL:DCL:double-checked locking 中文一般译为“双重检查锁
        RLock hotCacheLock=redisson.getLock(LOCK_PRODUCT_HOT_CACHE_CREATE_PREFIX+productId);
        hotCacheLock.lock();
        try{
            product =getProductFormCVache(producyCacheKey);
            if(product!=null){
                return product;
            }

            product = productDao.findProductById(productId);
            if(product!=null){
                redisUtil.set(producyCacheKey,JSON.toJSONString(product),PRODUCT_CACHE_TIMEOUT, TimeUnit.SECONDS);
            }else{
                redisUtil.set(producyCacheKey,EMPTY_CACHE);
            }
        }finally {
            hotCacheLock.unlock();
        }
        return  product;
    }

(4)双写不一致

场景介绍:

线程3查了数据stock=10, 此时线程2写入stock=6,且先更新缓存stock=6.

此时线程3再去更新缓存使用的是旧值stock=10.这就是双写不一致问题。

update|delete缓存都有这个问题

https://blog.csdn.net/weixin_42201180/article/details/129244404

当前的代码也存在读写不一致 问题。

3-4种方案可以来解决

1.分布式锁:

同一个商品,一次一个人来改。锁粒度要尽可能小,锁太大会影响性能

    public static final Integer PRODUCT_CACHE_TIMEOUT = 60*60*24;
    public static final String EMPTY_CACHE = "{}";
    public static final String LOCK_PRODUCT_HOT_CACHE_CREATE_PREFIX = "lock:product:hot_cache_create";
    public static final String LOCK_PRODUCT_UPDATE_PREFIX = "lock:product:update";

    @Transactional
    public Product update(Product product){
        Product productResult=null;
        RLock updateProductLock = redisson.getLock(LOCK_PRODUCT_UPDATE_PREFIX+product.getId());
        updateProductLock.lock();
        try{
            productResult = productDao.update(product);
            redisUtil.set(RedisKeyPrefixConst.PRODUCT_CACHE+productResult.getId(), JSON.toJSONString(productResult));

        }finally {
            updateProductLock.unlock();
        }
        return  productResult;
    }

改进一:

查询和修改串行进行:同一个商品排队执行,对于性能有损害

public Product get2(Long productId){
        Product product=null;
        String producyCacheKey = RedisKeyPrefixConst.PRODUCT_CACHE+productId;
        //DCL:DCL:double-checked locking 中文一般译为“双重检查锁
        RLock hotCacheLock=redisson.getLock(LOCK_PRODUCT_HOT_CACHE_CREATE_PREFIX+productId);
        hotCacheLock.lock();
        //查
        try{
            product =getProductFormCVache(producyCacheKey);
            if(product!=null){
                return product;
            }
            //改
            RLock updateProductLock=redisson.getLock(LOCK_PRODUCT_UPDATE_PREFIX+productId);
            updateProductLock.lock();
            try{
                if(product!=null){
                    redisUtil.set(producyCacheKey,JSON.toJSONString(product), getProductCacheTimeout(), TimeUnit.SECONDS);
                }else{
                    redisUtil.set(producyCacheKey,EMPTY_CACHE, getProductCacheTimeout(), TimeUnit.SECONDS);
                }

                product = productDao.findProductById(productId);
                if(product!=null){
                    redisUtil.set(producyCacheKey,JSON.toJSONString(product),PRODUCT_CACHE_TIMEOUT, TimeUnit.SECONDS);
                }else{
                    redisUtil.set(producyCacheKey,EMPTY_CACHE);
                }
            }finally {
                updateProductLock.unlock();
            }
        }finally {
            hotCacheLock.unlock();
        }
        return  product;
    }

分段锁:用不了,同一个资源没法分

2.读写锁:读多写少;读写互斥,读锁不互斥并行

            RReadWriteLock readWriteLock=redisson.getReadWriteLock(LOCK_PRODUCT_UPDATE_PREFIX+productId);
            RLock rLock=readWriteLock.readLock();
            rLock.lock();
            try{
                if(product!=null){
                    redisUtil.set(producyCacheKey,JSON.toJSONString(product), getProductCacheTimeout(), TimeUnit.SECONDS);
                }else{
                    redisUtil.set(producyCacheKey,EMPTY_CACHE, getProductCacheTimeout(), TimeUnit.SECONDS);
                }

                product = productDao.findProductById(productId);
                if(product!=null){
                    redisUtil.set(producyCacheKey,JSON.toJSONString(product),PRODUCT_CACHE_TIMEOUT, TimeUnit.SECONDS);
                }else{
                    redisUtil.set(producyCacheKey,EMPTY_CACHE);
                }
            }finally {
                rLock.unlock();
            }

写锁:

    @Transactional
    public Product update(Product product){
        Product productResult=null;
        //RLock updateProductLock = redisson.getLock(LOCK_PRODUCT_UPDATE_PREFIX+product.getId());
        //updateProductLock.lock();
        RReadWriteLock readWriteLock=redisson.getReadWriteLock(LOCK_PRODUCT_UPDATE_PREFIX+product.getId());
        RLock writeLock=readWriteLock.writeLock();
        writeLock.lock();
        try{
            productResult = productDao.update(product);
            redisUtil.set(RedisKeyPrefixConst.PRODUCT_CACHE+productResult.getId(), JSON.toJSONString(productResult));

        }finally {
            writeLock.unlock();
        }
        return  productResult;
    }

改进二:

锁一:突发性热点问题DCL_缓存重建,第一个人缓存,其他人不需要缓存重建

串行转并并发

改进三:

锁一和锁二能不能合并

(5)缓存雪崩

场景介绍

有个粉丝量很大的大V账号,发了一个微博消息之后,微博挂了。

原因是:大V发的一条消息,只会落在分布式系统的一个节点上,当大量的粉丝访问这个节点时,就会造成缓存雪崩问题。

【Redis篇】Redis缓存之缓存雪崩_redis缓存雪崩-CSDN博客

Redis--缓存雪崩及解决方案_redis缓存雪崩-CSDN博客

Redis 面试常见问题:缓存雪崩、缓存击穿以及缓存穿透-腾讯云开发者社区-腾讯云

解决方法:多级缓存策略

    private Map<String,Product> productMap=new ConcurrentHashMap<>();
    public Product get(Long productId){
        Product product=null;
        String producyCacheKey = RedisKeyPrefixConst.PRODUCT_CACHE+productId;
        //DCL:DCL:double-checked locking 中文一般译为“双重检查锁
        RLock hotCacheLock=redisson.getLock(LOCK_PRODUCT_HOT_CACHE_CREATE_PREFIX+productId);
        hotCacheLock.lock();
        try{
            product =getProductFormCVache(producyCacheKey);
            if(product!=null){
                return product;
            }

            product = productDao.findProductById(productId);
            if(product!=null){
                redisUtil.set(producyCacheKey,JSON.toJSONString(product),PRODUCT_CACHE_TIMEOUT, TimeUnit.SECONDS);
                productMap.put(producyCacheKey,product);
            }else{
                redisUtil.set(producyCacheKey,EMPTY_CACHE);
            }
        }finally {
            hotCacheLock.unlock();
        }
        return  product;
    }

    @Transactional
    public Product update(Product product){
        Product productResult=null;
        //RLock updateProductLock = redisson.getLock(LOCK_PRODUCT_UPDATE_PREFIX+product.getId());
        //updateProductLock.lock();
        RReadWriteLock readWriteLock=redisson.getReadWriteLock(LOCK_PRODUCT_UPDATE_PREFIX+product.getId());
        RLock writeLock=readWriteLock.writeLock();
        writeLock.lock();
        try{
            productResult = productDao.update(product);
            redisUtil.set(RedisKeyPrefixConst.PRODUCT_CACHE+productResult.getId(), JSON.toJSONString(productResult));
            productMap.put(RedisKeyPrefixConst.PRODUCT_CACHE+productResult.getId(),productResult);
        }finally {
            writeLock.unlock();
        }
        return  productResult;
    }

 private Product getProductFormCVache(String productCacheKey){

        Product product=productMap.get(productCacheKey);
        if(product!=null){
            return product;
        }
        String productStr= redisUtil.get(productCacheKey);
        if(!StringUtils.isEmpty(productStr)){
            if(EMPTY_CACHE.equals(productStr)){
                return null;
            }
            product=JSON.parseObject(productStr,Product.class);
        }
        return product;
    }

改进:

多级缓存还有可能存在的问题:内存比redis性能更高,问题是只更新一台服务器,其他服务器怎么办,还是有数据不一致问题。

多级缓存还可能存在的问题

1.缓存不够放,内存溢出问题,堆内存

缓存框架,使用淘汰策略,淘汰不用的缓存

2.单机map,其他服务器怎么同步

借助mq更新其他缓存,让其他人监听,然后更新自己的数据,

3.思考

耦合太多插件,虽然解决了问题,但不优雅

解决数据不一致问题:如何优雅呢?让热点缓存框架,实现更加优雅?