菜单 学习猿地 - LMONKEY

VIP

开通学习猿地VIP

尊享10项VIP特权 持续新增

知识通关挑战

打卡带练!告别无效练习

接私单赚外块

VIP优先接,累计金额超百万

学习猿地私房课免费学

大厂实战课仅对VIP开放

你的一对一导师

每月可免费咨询大牛30次

领取更多软件工程师实用特权

入驻
52
0

Redis学习笔记2—缓存、集群、一致性等

原创
05/13 14:22
阅读数 1179

缓存淘汰策略

为了保证高性能,缓存都保存在内存中,当内存满了之后,需要通过适当的策略淘汰老数据,以便腾出空间存储新数据。数据的淘汰策略,典型的包括FIFO(先进先出,淘汰最老数据),LRU(淘汰最近最少使用的),LFU(淘汰使用频率最低的)。

FIFO很简单就不展开了,主要说下LRU和LFU的区别,详细区别参考这里

  1. LRU(Least Recently Used),首先淘汰最长时间未被使用的数据。实现方法是每次访问数据后把数据移到队头,删除时从队尾开始删除。
  2. LFU(Least Frequently Used),首先淘汰一定时期内被访问次数最少的数据。实现方法是记录数据在一定时段内的访问评率,删除访问频率最低的数据。此算法需要额外维护每个数据的访问量,并排序,实现比较复杂。

Java的LinkedHashMap已经实现了LRU算法,具体实现请查看JDK源码,使用方法请仔细阅读LinkedHashMap以下两个方法的JavaDoc(我贴出来了,注释有点多,有删减)。

    /**
     * Constructs an empty <tt>LinkedHashMap</tt> instance with the
     * specified initial capacity, load factor and ordering mode.
     *
     * @param  initialCapacity the initial capacity
     * @param  loadFactor      the load factor
     * @param  accessOrder     the ordering mode - <tt>true</tt> for
     *         access-order, <tt>false</tt> for insertion-order
     * @throws IllegalArgumentException if the initial capacity is negative
     *         or the load factor is nonpositive
     */
    public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }
    /**
     * Returns <tt>true</tt> if this map should remove its eldest entry.
     *
     * <p>This implementation merely returns <tt>false</tt> (so that this
     * map acts like a normal map - the eldest element is never removed).
     *
     * @param    eldest The least recently inserted entry in the map, or if
     *           this is an access-ordered map, the least recently accessed
     *           entry.  
     * @return   <tt>true</tt> if the eldest entry should be removed
     *           from the map; <tt>false</tt> if it should be retained.
     */
    protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
        return false;
    }

Redis提供的淘汰策略包括如下几种:

  1. noeviction(内存满后不主动回收,无法写入新数据)
  2. allkeys-lru(最近最少使用的Key)
  3. allkeys-random(随机回收)
  4. volatile-lru(过期集合中最近最少使用的Key)
  5. volatile-random(过期随机回收)
  6. volatile-ttl(过期最短存活)

常见缓存性能问题

缓存穿透

缓存穿透是指去获取一个Redis和DB都不存在的数据,由于Redis中不存在,导致流量透传到DB,而DB中无相关数据,查询后不会缓存结果到Redis。如果大量此类查询,会给数据库带来性能风险,此问题可被攻击者利用。

避免方法:

  1. 对于DB中查询不到的数据,也在Redis中进行短期缓存,避免反复查询DB。
  2. 使用互斥锁(mutex key):到缓存没命中时,不是立即去查询DB,而是先获取一个互斥锁(SETNX命令),获取到锁成功后再去查询DB。
  3. 业务层增加校验,过滤非法数据。
public String get(key) {  
    String value = redis.get(key);  
    if (value == null) { //代表缓存值过期  
       //设置超时,防止del失败时死锁  
       if (redis.setnx(key_mutex, 1, 60) == 1) { //代表设置成功  
           value = db.get(key);  
           redis.set(key, value, expire_secs);  
           redis.del(key_mutex);  
        } else {  //没获取到锁,表明其他线程获取了,等待一段时间后重试查询缓存  
           sleep(50);  
           get(key); //重试  
        }  
    } else {  
        return value;  
  }  
}

以上代码参考自Redis 的key设计技巧&&缓存问题

缓存击穿

缓存击穿是指在某个热点Key过期的时候,客户端产生大量的情况,导致请求击穿缓存直接到达DB,给DB带来巨大压力,避免方法请参考上述缓存穿透的互斥锁

缓存雪崩

缓存雪崩是指缓存服务器重启时或者大量缓存在短时间内集中过期时,恰好此时大量客户端执行并发操作,缓存命中失败导致给DB带来巨大压力。

避免方法:

  1. 查询缓存失败后,查询数据库的代码先加锁再查询数据库,或者队列执行,对于每个Key,每个进程同时只允许一个线程访问数据库,减轻数据库压力。
  2. 参考上述缓存穿透的互斥锁
  3. 给每个Key的过期时间后面加个随机值,确保缓存不会在同一时刻大面积失效。
  4. 设置热点数据永不过期,数据更新后,主动刷新缓存。

缓存和数据一致性

多服务写Redis

首先,多个服务修改同一个Key是不好的设计模式,应该把维护同一个Key的操作集中到一个服务里,比如更新订单的状态,应该都转发给订单服务来操作。当多写情况无法避免时,应采取如下措施:

  1. 互斥写,通过Redis的setex实现互斥说,来竞争对Key的写入权限。
  2. 使用乐观锁,给数据添加版本号或时间戳,通过乐观锁判断是否可以写入。

Redis & DB一致性

Redis作为缓存使用的时候,一般都是DB数据的映像,两套系统就会存在数据不一致的情况,如何才能最大限度的降低数据不一致的影响呢?比如数据库刚写入一个更新,缓存更新命令还没执行,这个时候来了个读请求,从缓存中读取的数据就不是最新的。

如果对于高一致性要求的场景,只能把读写操作串行执行,确保缓存和DB的一致性,但这样会严重降低系统的吞吐量,甚至成为系统瓶颈。

更通用的保存缓存和DB数据一致性的做法,是DB写入数据库后,删除缓存数据。这样系统下次读取请求时,会从DB中读取最新的数据并进行缓存。采用删除而不是更新缓存,主要是基于性能的考虑,不然如果反复更新数据场景下,反复写无人消费的缓存数据是一种浪费。

Redis集群部署

Redis支持主从和分片两种Cluster部署模式,提供高可用性。

主从

在主备模式下,Redis通过Sentinel哨兵来监控Master的状态,当Master异常后,从从节点中选出新主节点,并调整其他从节点的slaveof到新Master。
Sentinel通过部署多实例,实例间通过 Raft协议 实现自身的高可用,所有Sentinel需要部署 3个 节点才能保持自身的健壮性。

在一主多从模式下,为了减轻Master的数据同步压力,可以把主从模式配置为主从链模式,即A是B的主,B是C的主,从而减轻B和C都从A同步数据的压力。

主从模式下,当有新节点加入时,流程如下:

  1. 新节点向Master发送psync命令
  2. Master执行bgsave,fork子进程,生成RDB数据,并缓存新数据
  3. Master把RDB发送给新节点恢复
  4. Master把缓存中的新数据发送给新节点
  5. 新节点初始化完成,后续通过AOF进行增量数据的同步

分片

分别模式先,Redis通过一致性Hash算法,在内部把数据切分为16384个slot。通过对数据的Key进行Hash计算来数据保存的分片位置。

数据持久化机制和恢复

Redis支持RDB和AOF两种持久化机制。
RDB是内存数据库快照,Redis通过fork子进程把内存存储(二进制压缩)为RDB文件。快照过程中,新增数据使用COW(copy on write)的模式写入。RDB适合做灾备,但是由于定时报错,容易丢失部分数据。
AOF(append only file)是以文本日志形式记录Redis每个写入和删除操作。AOF日志写入支持灵活的策略,如每秒刷盘,根据数据量刷盘等。
RDB是最新数据快照,文件小,AOF记录操作过程,文件比较大。数据恢复时,一般采用RDB+AOF的模式来实现,RDB作为基准数据,叠加快照之后的AOF数据,完成完整的数据恢复。

Keys和Scan方法

Keys会全表扫描,对于单线程的Redis,容易出现卡顿,影响性能。
Scan采用类似分页获取的方式,虽然实现代码复杂一点,而且有可能数据重复,但是不会有性能问题。
一般生产库,都会禁用keys命令。

Redis vs Memcached

特性 Redis Memcached
性能 单线程非阻塞异步IO,避免线程切换 多线程异步IO,可利用多核
持久化 支持,可作为NoSQL数据库 不支持,有效期最长30天
数据结构 支持5种 只支持简单数据结构
限制 - Key 250字节,Value 1M,缓存30天
HA 主从、Cluster 不支持

发表评论

0/200
52 点赞
0 评论
收藏
为你推荐 换一批