八、Redis缓存运用问题

一、数据一致性问题

如果缓存里的数据和数据库里的数据不一致,那么应用程序从缓存读取的数据是一个错误的数据。

1、新增数据的一致性问题

如果有业务需要在新增数据时,写入redis和mysql,可以使用消息中间件推送给redis或mysql,这样即使出现消费失败也可以自动重发,从而避免数据不一致问题。所以新增数据的不一致性很少发生。

2、修改/删除的一致性问题

在修改数据时,比较容易出现数据不一致问题。基本的修改数据方法有以下几种:

1、先更新数据库,后更新缓存。

2、先更新缓存,后更新数据库。

3、先更新数据库,后删除缓存。

4、先删除缓存,后更新数据库。

3、更新数据库和缓存

4、先删除缓存,后更新数据库

5、先更新数据库,后删除缓存

根据更新数据的流程图可知:线程在执行更新操作时,无法保证执行的先后顺序,比较容易出现数据不一致问题。

但是出现数据不一致问题概率不高,由于一般情况下更新数据库的耗时要多于查询数据库,所以出现上图的情况很少。推荐使用。

二、延迟双删

这时我们该怎么办呢?

有一个方法是先删除缓存,更新完数据库以后,再删除一次缓存。这样下一个线程读取的时候判断缓存为空,就到数据库读取数据并且更新缓存。这种方法叫延迟双删。

为什么要延迟?

为了确保其他已读取到数据库旧数据但是没写入redis缓存的线程执行完成了。

// 先删除缓存
redis.del(key);

// 更新数据库
dataBase.update();

// 等待1000ms
Thread.sleep(1000);

// 再删除缓存
redis.del(key);

1、数据库读写分离时的延迟双删

在一些项目中可能会遇到数据库是读写分离架构,在对主库写入数据后,从库要同步这些数据,这个过程需要消耗一定的时间。所以如果使用延迟双删的方案,需要等待从库同步完数据后,再做第二次删除缓存。所以Thread.sleep需要延长等待时间。

如果不想延长线程睡眠时间,那么可以让读数据库的操作从主库读取数据。这样不必等待从库同步完数据。

2、延迟双删的缺点

由于延迟双删的机制是等待一定的时间后再执行,这样无端延长了线程的执行时间,降低响应速度。

解决办法也有,可以使用异步操作实现延迟双删,比如启动一个子线程等待1000ms,或者使用rocketMQ的延迟消息。这样对主线程的响应速度没影响。

3、异步延迟双删

可以使用RocketMQ的延迟消息功能,异步实现删除Redis缓存。

4、消息中间件的异步延迟双删的缺点

如果原本系统中没加入消息中间件,为了数据一致性加一个消息中间件,代价比较大。

5、订阅binlog:canal

canal是阿里提供的异步双删的组件,不用自己再开发。

三、缓存穿透

1、概念

进入系统的请求去查询不存在的数据,先到redis没查到,又到数据库也没查到。大量这样的请求会给数据库造成很大的压力。

2、解决方案

① 缓存空值

如果redis缓存没查到数据,数据库也没查到数据,就向redis放一个value为null的数据,这样相同的请求再进来的时候就可以从redis中获取一个null,避免数据库压力过大。

② 布隆过滤器

redis提供一个数据类型是:bitMap,底层是一个二进制数组,可以记录某些key。

当过来一个key的时候,使用布隆过滤器计算是否属于这个集合,如果不属于就返回null,避免请求进入数据库。

四、缓存击穿

1、概念

热点key过期的瞬间,大量的请求同时涌入数据库,压垮数据库。

2、解决方案

① 针对热点key,不设置过期时间。

② 加入分布式锁,只允许一个请求进入数据库,其他请求等待缓存恢复。

/**
 * 防止缓存击穿的代码写法
 *
 * @Author: AllWe
 * @Date: 2024/09/06/11:19
 */
@Slf4j
@Component
public class GetValue {

    @Resource
    private JedisPool jedisPool;

    @Resource
    private DataBaseTest dataBaseTest;

    private static final String LUA = "return redis.call('SET', KEYS[1], ARGV[1], 'EX', ARGV[2], 'NX')";

    private static final Long EXPIRE_TIME = 300L;

    private String getValue(String key) {
        Jedis jedis = null;
        String value = null;

        try {
            jedis = jedisPool.getResource();
            value = jedis.get(key);
            // 如果查询结果为空 - 缓存过期
            if (StrUtil.isBlank(value)) {
                // 注意这里加入一个互斥操作,使用了NX参数,确保只有一个线程进入数据库查询,将查询结果放回缓存
                if("OK".equals(jedis.eval(LUA, Collections.singletonList("key_mutext"), Arrays.asList("1", "180")))) {
                    value = dataBaseTest.get(key);
                    jedis.setex(key, EXPIRE_TIME, value);
                    jedis.del("key_mutext");
                } else {
                    // 等待进入数据库查询的线程将数据放入缓存 - 50ms
                    Thread.sleep(50);
                    // 再次从缓存取数据
                    value = jedis.get(key);
                }
            }
        } catch (Exception e) {
            log.info("查询异常!");
        }
        return value;
    }
}

五、缓存雪崩 - stampeding herd

1、概念

redis中大量的缓存失效,导致大量的请求需要直接访问数据库。缓存这一层基本崩溃了。

2、解决方案

错开缓存的失效时间,避免同一时刻失效,可以加一个随机过期时间。

六、服务降级

在并发特别高的时候,可以关掉一些不重要的功能,将资源让给重要的功能。

七、数据倾斜问题 - 热点key问题

1、产生原因

在一个应用程序中,有大量的请求去访问个别的几个key,导致只有少量的redis节点去处理这些请求,而其他的节点没参与运算。造成访问倾斜,运算压力不平衡。

比如微博因为某某明星结婚导致请求集中,扛不住压力崩溃。

2、危害

请求太多会占满某一台redis服务器的带宽。

可能导致缓存穿透、击穿、雪崩等情况。

建议:单台redis节点存储的数据量别太大,最好别超过8GB,否则在主从架构中同步数据压力会很大。

3、发现热点key

在电商平台可以对一些商品做预估,提前准备一些热点key。

但是对于社交平台,比如微博,无法预测会出现哪些热点数据,这时候该怎么办呢?

① 客户端使用 concurrentHashMap

在应用程序代码中记录key的访问次数,每访问一次value +1,提前设定阈值 = 100W,到达阈值的判定为热点key。

优点:实现简单。

缺点:

1、对业务程序侵入大。

2、如果有其他程序共享了相同redis数据库的相同key,这时也需要在其他程序中维护相同的功能,开发成本高。

3、如果记录的key数量很多,会导致concurrentHashMap很大,占用内存太多。

② Redis 的 monitor 命令

使用monitor命令可以实现对redis中的所有命令监控,通过监控到的历史命令获取热点key。

redis-faina就是使用monitor实现对redis的分析。

优点:可以不侵入业务代码的方式获取热点key。

缺点:

1、给了redis一个额外的任务,造成了额外的开销,redis性能下降。

2、针对redis集群,需要额外的汇总工作。

3、适合短时间内监控redis数据的走向,给开发人员提供一些信息,用完再关闭。

③ Redis 的 hotkeys

在4.0.3版本之后发布,专门统计热点key。

# 查询redis热点key
./redis.cli --hotkeys

缺点:

1、不能按自定义规则查询热点key,只能按redis默认的规则查询。

2、是一个全局命令,会把所有的key扫描出来,最后再汇总。如果key的数量很多,会很消耗redis性能。

④ 抓取TCP的包

在redis的服务器上面抓取TCP的包,实现对热点key的统计。

优点:不侵入应用程序。

缺点:

1、开发、学习成本高:ES -> ELK体系提供了packetbeat插件,实现了对redis数据的分析。

2、可能导致服务器网络波动,有丢包的可能。

3、维护成本高。

4、解决热点key

1、使用二级缓存,使用Guava-cache,Hcache,JVM对象等方式将热点数据保存到内存中,不必再通过网络访问redis服务,提升响应速度。

2、分散热点key的热度。1拆分热点key的数据,可以将一个热点key拆分成多个子key,如果是集群就分散存储到不同的redis节点上,这样每个子key只分担了原来一小部分的请求。

八、数据倾斜问题 - bigKey问题

1、概念

某个key对应的value值太大,超出预设的阈值,就认为这个key是bigKey。一般有两个分类:

String:占用空间达到阈值。

非String:hash、list、set、zset,元素数量到达阈值。

value值太大或元素太多在进行操作的时候时间复杂度会很高。

2、危害

造成数据量倾斜,使每个节点的负载不同。

redis内部是单线程执行,在操作bigKey的时候阻塞时间太长,可能导致请求超时。

网络拥塞。如果一个key是bigKey的同时还是一个热点key,这时每秒响应的数据量太大,造成网络拥塞。

3、发现bigKey

① redis全局命令

redis-cli --bigkeys

全局扫描,效率低下,不推荐使用。

不能按自定义阈值做判断。

② debug 命令

debug object 'key'

通过debug命令可以读取到指定key对应的value详细信息,比对serializedlength参数的值可以获取value的长度,如果超出定义的阈值就在应用程序中标记为bigKey。

优点:可以按自定义阈值判断哪些key是bigKey。

缺点:必须先知道要查询哪个key。

③ scan 命令

# scan cursor MATCH pattern COUNT count

scan 0 count 5

渐进式扫描。

scan命令支持分页扫描key,避免了keys * 的全局扫描,对redis缓存的性能消耗比较小。

优点:结合scan命令和debug命令发现bigKey的好处是避免了全局扫描,也支持自定义阈值设置。

缺点:由于每一次scan是独立执行的,在这个过程中如果新增了新的key,是不会扫描到的,导致最终的结果不是精确的。

当精度缺失在可以容忍的范围内时,如果有主从架构,可以在从节点上做这样的数据分析工作,不去影响主节点的工作性能。

4、解决bigKey

拆分:将一个bigKey的value分散到不同的key上,分散存储压力。

九、脑裂问题

在主从-哨兵架构下,由于网络波动,可能出现主节点掉线后,哨兵节点从从节点中选举出来两个主节点,这时称为脑裂问题。

redis如何解决脑裂问题?在conf配置文件中配置相关参数可以减少脑裂问题的发生。

min-replicas-to-write 3:最小副本写入数量。

min-replicas-max-lag 10:最小副本范围内最大延迟时间。

说人话:当一个应用程序向redis集群中写入数据时,要求最少向3台redis节点写入成功,且这三台redis节点写入的延迟不超过10s。否则,写入失败。

基于CAP理论,在分布式系统中,不可能完全杜绝脑裂问题的发生。

但是在redis-cluster中可以通过修改配置文件参数:cluster-require-full-coverage yes 来开启集群保护机制,使得当一个分配了槽位的主节点掉线时集群进入保护状态,整个集群不可用,不会再写入新数据,从而减少脑裂问题。

而且在搭建redis集群的时候要求主节点数量尽量是奇数,且最少节点数是3,进一步减少脑裂问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值