Redis常见问题指北

编者注:笔者整理了一份【Redis不完全指南】,包含了很多详细的知识点和Redis经典面试题,可关注「TopCoder」公众号,发送 Reids 来获取~

Redis有哪些数据结构

5种基本类型:字符串String、字典Hash、列表List、集合Set、有序集合SortedSet。

如果你使用的是较新版本的Redis,还需要加上下面几种数据结构HyperLogLog(用于统计非重复计数,类似于取set.size,set用于保存计数项,不过HyperLogLog占用内存较小,可用于大型网站的UV统计)、Geo、Pub/Sub、BloomFilter等。

Redis分布式锁是什么

先拿setnx来争抢锁,抢到之后,再用expire给锁加一个过期时间防止锁忘记了释放。当然这样如果在setnx之后执行expire之前进程意外crash或者要重启维护了会有隐患,可以使用setnx命令直接加上过期时间。

不过redis分布式锁存在锁续约问题,可以在加锁期间针对锁不断进行续约。如果业务能够保证在加锁时间范围内执行完毕,那么理论上不需要进行锁续约。一般来说,Redis分布式锁key对应的value要保证操作的唯一性,避免程序unlock了不属于自己lock的锁。分布式锁除了基于Redis的实现之外,还可以基于DB或者zookeeper。

假如Redis里面有1亿个key,其中有50w个key是以某个固定的已知的前缀开头的,如果将它们全部找出来?

使用keys指令可以扫出指定模式的key列表。

因为Redis是单线程的,keys指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用scan指令,scan指令可以无阻塞的提取出指定模式的key列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用keys指令长。(scan原理是分批次扫描hash表中的slot进行查找,批次最小单位为slot,有可能返回值重复是因为扫描时间内发生了rehash缩容操作)

Redis可以作为异步队列么,怎么用

一般使用list结构作为队列,rpush生产消息,lpop消费消息。当lpop没有消息的时候,要适当sleep一会再重试。如果应用不想用sleep呢,其实list还有个指令叫blpop,在没有消息的时候,它会阻塞(超时)住直到消息到来。

Redis能不能实现生产一次消费多次呢,类似于消息的广播

使用pub/sub主题订阅者模式,可以实现1:N的消息队列。Redis5.0增加一个stream结构,用于消息发布订阅。注意:Redis的发布订阅模式只能针对在线的client,并且没有消息进度机制,因此client断线时无法接收到消息,client断线重连后历史消息无法消费。

那么pub/sub有什么缺点

在消费者下线的情况下,生产的消息会丢失,得使用专业的消息队列如rabbitmq/rocketmq等。

Redis如何实现延时队列

Redis本身不支持延时队列,不过可以借助于sortedSet来实现。使用sortedset,拿时间戳作为score,消息作为key/value,调用zadd来生产消息,消费者用zrangebyscore指令获取N秒之前的数据轮询进行处理。

如果有大量的key同一时间过期会怎么样

如果大量的key过期时间设置的过于集中,到过期的那个时间点,redis可能会出现短暂的卡顿现象。一般需要在时间上加一个随机值,使得过期时间分散一些。

Redis如何做持久化的

bgsave(RDB)做镜像全量持久化,aof做增量持久化。因为bgsave会耗费较长时间,不够实时,在停机的时候会导致大量丢失数据,所以需要aof来配合使用。在redis实例重启时,优先使用aof进行恢复,aof文件不存在时会使用rdb文件恢复。

注意,aof在rewrite时也是做全量持久化的,会耗费不少时间。执行rdb和aof重写都是fork子线程来完成的,避免影响主线程。

如果突然机器掉电会怎样

取决于aof日志sync属性的配置,如果不要求性能,在每条写指令时都sync一下磁盘,就不会丢失数据。但是在高性能的要求下每次都sync不现实,一般都使用定时sync,比如1s1次,这个时候最多就会丢失1s的数据。

bgsave的原理是什么

fork和cow(写时复制)。fork是指redis通过创建子进程来进行bgsave操作,cow指的是copy on write,子进程创建后,父子进程共享数据段,父进程继续提供读写服务,写脏的页面数据会逐渐和子进程分离开来。

Pipeline有什么好处,为什么要用pipeline

可以将多次IO往返的时间缩减为一次,前提是pipeline执行的指令之间没有因果相关性。使用redis-benchmark进行压测的时候可以发现影响redis的QPS峰值的一个重要因素是pipeline批次指令的数目。

Redis的同步机制了解么

Redis可以使用主从同步,从从同步。第一次同步时,主节点做一次bgsave,并同时将后续修改操作记录到内存buffer,待完成后将rdb文件全量同步到复制节点,复制节点接收完成后将rdb镜像加载到内存。加载完成后,再通知主节点将期间修改的操作记录同步到复制节点,这样就完成了同步过程。

注意,Redis的复制都是异步执行的。Redis有2个参数:min-slaves-to-write和min-slaves-max-lag,二者表示的含义为:如果至少有 min-slaves-to-write 个从服务器, 并且这些服务器的延迟值都少于 min-slaves-max-lag 秒, 那么主服务器就会执行客户端请求的写操作,否则拒绝写操作。这在一定程度上减小主从不同步的时间间隔,但是没有从根本上解决同步一致性问题。

是否使用过Redis集群,集群的原理是什么?

Redis Sentinal着眼于高可用,在master宕机时会自动将slave提升为master,继续提供服务。Redis Cluster着眼于扩展性,在单个redis内存不足时,使用Cluster进行分片存储。

(这里可能涉及到一致性哈希,后续分析下一致性哈希原理,一致性哈希原理可参考https://crossoverjie.top/2018/01/08/Consistent-Hash/,一致性哈希实现可参考https://www.cnblogs.com/xrq730/p/5186728.html)

Redis过期策略

过期策略通常有以下三种:

  • 定时过期:每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。
  • 惰性过期:只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
  • 定期过期:每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。(expires字典会保存所有设置了过期时间的key的过期时间数据,其中,key是指向键空间中的某个键的指针,value是该键的毫秒精度的UNIX时间戳表示的过期时间。键空间是指该Redis集群中保存的所有键。)

Redis中同时使用了惰性过期和定期过期两种过期策略

Redis定期过期策略

serverCron是由redis的事件框架驱动的定位任务,这个定时任务中会调用activeExpireCycle函数,针对每个db在限制的时间REDIS_EXPIRELOOKUPS_TIME_LIMIT内迟可能多的删除过期key,之所以要限制时间是为了防止过长时间 的阻塞影响redis的正常运行。这种主动删除策略弥补了被动删除策略在内存上的不友好。

因此,Redis会周期性的随机测试一批设置了过期时间的key并进行处理。测试到的已过期的key将被删除。典型的方式为,Redis每秒做10次如下的步骤:

  1. 随机测试100个设置了过期时间的key
  2. 删除所有发现的已过期的key
  3. 若删除的key超过25个则重复步骤1。(注意,处理过期耗时不能超过REDIS_EXPIRELOOKUPS_TIME_LIMIT)

Redis内存淘汰策略

Redis的内存淘汰策略是指在Redis的用于缓存的内存不足时,怎么处理需要新写入且需要申请额外空间的数据。

  • noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。
  • allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。
  • allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。
  • volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。
  • volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。
  • volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。

对于LRU(Least Recent Used),淘汰掉最不经常使用的,对于LRU的实现hashMap + 双向链表来实现,如果Redis也基于hashMap + 双向链表实现,显然要对目前的数据结构做较大改动,为了追求空间的利用率,Redis采用权衡的实现方案:Redis会基于server.maxmemory_samples配置选取固定数目的key,然后比较它们的lru访问时间,然后淘汰最近最久没有访问的key,maxmemory_samples的值越大,Redis的近似LRU算法就越接近于严格LRU算法,但是相应消耗也变高,对性能有一定影响,样本值默认为5

关于LRU算法和Redis中的LRU实现可参考:https://zhuanlan.zhihu.com/p/34133067、https://www.cnblogs.com/Lifehacker/p/redis_random_lru.html和https://zhuanlan.zhihu.com/p/34133067。

Redis事务了解不

Redis通过MULTI、EXEC、WATCH命令来实现事务,Reids在事务执行期间,服务器不会中断事务去执行其他命令,而是在事务的所有命令执行完毕后再执行其他命令,因为Redis是单线程处理模型。

Redis事务和传统的数据库事务最大的区别就是:Reids事务不具有回滚机制,即使事务中有部分命令执行错误,事务也会继续执行之后的命令直到结束,并且之前执行的命令不受任何影响。Redis为什么不支持回滚机制呢,其作者解释道,不支持事务回滚是因为这种复杂的功能和Reids追求的简单高效设计主旨不符,并且他认为,Reids事务的执行中的错误通常是由程序错误导致的,这种错误在实际生产环境中较少出现,因此没必要为Reids开发事务回滚功能。

一致性hash算法

构造一个长度为2^32的整数环(这个环称为一致性hash环),根据节点名称的Hash值(其分布为[0, 2^32-1])将服务器节点放置在这个Hash环上,然后根据数据的Key值计算得到其Hash值(其分布也为[0, 2^32-1]),接着在Hash环按照顺时针(或者逆时针)查找距离这个Key值的Hash值最近的服务器节点,完成Key到服务器的映射查找。

一致性hash算法解决了节点伸缩(比如服务器上下线)时尽量将更多的请求命中原来路由到的服务器,减小由于节点伸缩对于原来映射关系的影响。一致性hash算法有两种实现,一种是未添加虚拟节点,另一种是添加了虚拟节点。后者由于使用了多个虚拟节点进行映射,理论上hash分散效果更好。

未添加虚拟节点的一致性hash算法实现:

private static int hash(String str) {
    final int p = 16777619;
    int hash = (int)2166136261L;
    for (int i = 0; i < str.length(); i++)
        hash = (hash ^ str.charAt(i)) * p;
    hash += hash << 13;
    hash ^= hash >> 7;
    hash += hash << 3;
    hash ^= hash >> 17;
    hash += hash << 5;
 
    // 如果算出来的值为负数则取其绝对值
    if (hash < 0)
        hash = Math.abs(hash);
    return hash;
}
 
private static TreeMap<Integer, String> treeMap = new TreeMap<>();
static {
    List<String> list = Arrays.asList("192.168.2.11", "192.168.2.12", "192.168.2.13");
    list.forEach(item -> treeMap.put(hash(item), item));
}
 
private static String route(String data) {
    Integer key = treeMap.higherKey(hash(data));
    if (key == null) {
        key = treeMap.firstKey();
    }
    return treeMap.get(key);
}
 
public static void main(String[] args) {
    List<String> list = Arrays.asList("aaa", "bbb", "ccc");
    list.forEach(item -> System.out.println(item + " 路由到了 " + route(item)));
}

添加虚拟节点的一致性hash算法实现:

private static TreeMap<Integer, String> treeMap = new TreeMap<>();
static {
    // 每个节点有10个虚拟节点
    List<Integer> vNode = Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9);
    List<String> list = Arrays.asList("192.168.2.11", "192.168.2.12", "192.168.2.13");
    list.forEach(item -> vNode.forEach(node -> treeMap.put(hash(item + node), item)));
 
    treeMap.forEach((key, value) -> System.out.println(key + ": " + value));
}

本文分享自微信公众号 - TopCoder(gh_12e4a74a5c9c)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-07-31

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券