Redis

作者:加菲猫 2018-06-19 306 0

为什么要是有 Redis?

性能:需要执行耗时特别久,且结果不频繁变动的SQL,就特别适合将运行结果放入缓存。

并发:在大并发的情况下,所有的请求直接访问数据库,数据库会出现连接异常。这时就需要使用 Redis 做一个缓冲操作,让请求先访问到 Redis,而不是直接访问数据库。

Redis 的缺点

缓存和数据库双写一致性问题:如果对数据有强一致性要求,不能放缓存。先更新数据库,再删缓存。因为可能存在删除缓存失败的问题,提供一个补偿措施即可,例如消息队列。

缓存雪崩问题:缓存同一时间大面积失效,这个时候又来了一波请求,结果请求都到了数据库上,导致数据库连接异常。

  • 给缓存的失效时间,加上一个随机值,避免集体失效。

  • 利用互斥锁,先获得锁,再去请求数据库。没得到锁,则休眠一段时间重试。但是吞吐量明显下降了。

  • 双缓存。我们有两个缓存,缓存A 和 缓存B。缓存A 的失效时间为20分钟,缓存B 不设失效时间。需要做缓存预热(项目启动前,先加载缓存)操作。

缓存击穿问题:故意去请求缓存中不存在的数据,导致所有的请求都到了数据库上,导致数据库连接异常。

  • 利用互斥锁,先获得锁,再去请求数据库。没得到锁,则休眠一段时间重试。

  • 异步更新策略,无论 key 是否取到值,都直接返回。value 值中维护一个缓存失效时间,缓存如果过期,异步起一个线程去读数据库,更新缓存。需要做缓存预热(项目启动前,先加载缓存)操作。

  • 提供一个能迅速判断请求是否有效的拦截机制。比如,利用布隆过滤器,内部维护一系列合法有效的 key。迅速判断出,请求所携带的 Key 是否合法有效。如果不合法,则直接返回。

缓存的并发竞争问题:同时有多个子系统去 set 一个 key。不推荐使用 Redis 的事务机制,因为 Redis 集群环境下,数据是分片的,当一个事务涉及多个 key 时,这些 key 可能不在一个 redis-server 上。

  • 如果对这个 key 操作,不要求顺序,使用分布式锁,抢到锁的做 set 操作。

  • 如果对这个 key 操作,要求顺序,使用队列。

单线程的 Redis 为什么这么快?

纯内存操作

单线程操作,避免了频繁的上下文切换

采用了非阻塞 I/O 多路复用机制

I/O 多路复用机制

小明开了一家快递公司,由于资金限制,只买了一辆车送快递。

经营方式一:雇佣多个快递员,客户每送来一个快递,就分给一个快递员,然后快递员开车送出。相当于传统的并发模型,每个 I/O 流都有一个新的线程管理。

经营方式二:雇佣一个快递员,客户送来的快递依次放在一个地方,然后由快递员依次送出,送好了再回来拿下一个。就是 I/O 多路复用,只有一个线程,通过跟踪每个 I/O 流的状态,来管理多个 I/O 流。

经过对比,经营方式二的效率更高。

  • 快递员 → 线程

  • 快递 → socket(I/O 流)

  • 快递的送达地点 → socket 的不同状态

  • 客户的送快递请求 → 来自客户端的请求

  • 经营方式 → 服务端运行的代码

  • 一辆快递车 → CPU 核数

Redis 的数据类型及使用场景

String:set/get 操作,value 可以是 String 也可以是数字。用于复杂的计数功能的缓存。

hash:value 存放的是结构化的对象,便于操作其中的某个字段。用于单点登录。

list:用于消息队列。

set:用于全局去重。利用交集、并集、差集等操作,计算共同喜好,全部的喜好,自己独有的喜好等功能。

sorted set:多了一个权重参数 score,集合中的元素能够按score进行排列。用于排行榜应用,取 TOP N 操作,范围查找。还可以用来做延时任务。

如何使用 Redis 来实现分布式锁?

public class RedisTool {

    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";
    private static final Long RELEASE_SUCCESS = 1L;

    /**
     * 尝试获取分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @param expireTime 超期时间
     * @return 是否获取成功
     */
    public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {        String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);        if (LOCK_SUCCESS.equals(result)) {            return true;
        }        return false;

    }
    
    /**
     * 释放分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @return 是否释放成功
     */
    public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";        Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));        if (RELEASE_SUCCESS.equals(result)) {            return true;
        }        return false;

    }

}

Redis 有几种持久化方式?优缺点是什么?

RDB 持久化:在指定的时间间隔内生成数据集的时间点快照。

优点:

  • RDB 保存了 Redis 在某个时间点上的数据集。这种文件非常适合用于进行备份。可以在最近的 24 小时内,每小时备份一次 RDB 文件,并且在每个月的每一天,也备份一个 RDB 文件。即使遇上问题,也可以随时将数据集还原到不同的版本。

  • RDB 非常适用于灾难恢复,它只有一个文件,并且内容都非常紧凑,可以在加密后将它传送到别的数据中心。

  • RDB 可以最大化 Redis 的性能:父进程在保存 RDB 文件时唯一要做的就是 fork 出一个子进程,然后这个子进程就会处理接下来的所有保存工作,父进程无须执行任何磁盘 I/O 操作。

  • RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。

缺点:

  • 如果需要尽量避免在服务器故障时丢失数据,那么不推荐使用 RDB。 虽然 Redis 允许设置不同的保存点来控制保存 RDB 文件的频率,但是,因为 RDB 文件需要保存整个数据集的状态,所以它并不是一个轻松的操作。可能会至少 5 分钟才保存一次 RDB 文件。在这种情况下,一旦发生故障停机, 就可能会丢失好几分钟的数据。

  • 每次保存 RDB 的时候,Redis 都要 fork() 出一个子进程,并由子进程来进行实际的持久化工作。在数据集比较庞大时,fork()可能会非常耗时。

AOF 持久化:记录服务器执行的所有写操作命令,在服务器启动时,通过重新执行这些命令来还原数据集。AOF 文件中全部以 Redis 协议的格式来保存,新命令会被追加到文件的末尾,Redis 还可以在后台对 AOF 文件进行重写,文件的体积不会超出保存数据集状态所需要的实际大小。

优点:

  • 可以设置不同的 fsync 策略,比如无 fsync,每秒钟一次 fsync,或者每次执行写入命令时 fsync。AOF 的默认策略为每秒钟 fsync 一次,在这种配置下,Redis 仍然可以保持良好的性能,并且就算发生故障停机,也最多只会丢失一秒钟的数据,fsync 会在后台线程执行,所以主线程可以继续处理命令请求。

  • AOF 文件是一个只进行追加操作的日志文件,即使日志因为某些原因而包含了未写入完整的命令,redis-check-aof 工具也可以轻易地修复这种问题。

  • Redis 可以在 AOF 文件体积变得过大时,自动地在后台对 AOF 进行重写一旦新 AOF 文件创建完毕,Redis 就会从旧 AOF 文件切换到新 AOF 文件,并开始对新 AOF 文件进行追加操作。

  • AOF 文件有序地保存了对数据库执行的所有写入操作,导出 AOF 文件也非常简单。如果不小心执行了 FLUSHALL 命令,但只要 AOF 文件未被重写,那么只要停止服务器,移除 AOF 文件末尾的 FLUSHALL 命令,并重启 Redis,就可以将数据集恢复到 FLUSHALL 执行之前的状态。

缺点:

  • 对于相同的数据集来说,AOF 文件的体积通常要大于 RDB 文件。

  • 根据所使用的 fsync 策略,AOF 的速度可能会慢于 RDB 。 在一般情况下, 每秒 fsync 的性能依然非常高, 而关闭 fsync 可以让 AOF 的速度和 RDB 一样快, 即使在高负荷之下也是如此。 不过在处理巨大的写入载入时,RDB 可以提供更有保证的最大延迟时间。

  • 因为个别命令的原因,导致 AOF 文件在重新载入时,无法将数据集恢复成保存时的原样。

Redis 的缓存失效策略

Redis 采用的是定期删除 + 惰性删除策略。

定期删除策略:默认每 100 ms 检查是否有过期的 key,但不是检查所有的 key,而是随机抽取检查。因此,如果只使用定期删除策略,会导致很多 key 到时间没有被删除。

惰性删除策略:获取某个 key 时,会检查这个 key 是否过期了,如果过期就直接删除。

Redis 为什么不使用定时删除策略?

定时删除需要一个定时器负责监视 key,过期则删除,十分消耗 CPU 资源。在大并发请求下,CPU 时间应该用在处理请求上,而不是删除 key 上。

Redis 的内存淘汰机制

Redis 的定期删除 + 惰性删除策略中,如果定期删除没删除 key,也没有请求这个 key,Redis 的内存就会越来越高,这就要采用内存淘汰机制了。

内存淘汰机制的配置在 redis.conf 中的 # maxmemory-policy volatile-lru。

  • noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。(一般不用)

  • allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key。(推荐)

  • allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个 key。(一般不用)

  • volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的 key。这种情况一般是把 redis 既当缓存,又做持久化存储的时候才用。(不推荐)

  • volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个 key。(不推荐)

  • volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的 key 优先移除。(不推荐)

Redis 集群,高可用,原理是什么?

集群:当业务量、数据量增加时,可以通过任意增加减少服务器数量来解决问题。

  • 高可用性:在主机挂掉后,自动故障转移,使前端服务对用户无影响。

  • 读写分离:将主机读压力分流到从机上。

高可用:当一台服务器停止服务后,对于业务及用户毫无影响。Redis 有两种数据持久化方式,可以在服务器出现问题后,快速恢复数据。

Redis缓存分片

使用 Redis 集群,把缓存放到不同的 redis-server 中。

发表评论

下一篇: 上一篇: