首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Redis常见问题指北

Redis常见问题指北

作者头像
luoxn28
发布2019-11-06 11:45:49
8100
发布2019-11-06 11:45:49
举报
文章被收录于专栏:TopCoderTopCoderTopCoder

编者注:笔者整理了一份【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));
}
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-07-31,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 TopCoder 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Redis有哪些数据结构
  • Redis分布式锁是什么
  • 假如Redis里面有1亿个key,其中有50w个key是以某个固定的已知的前缀开头的,如果将它们全部找出来?
  • Redis可以作为异步队列么,怎么用
  • Redis能不能实现生产一次消费多次呢,类似于消息的广播
  • 那么pub/sub有什么缺点
  • Redis如何实现延时队列
  • 如果有大量的key同一时间过期会怎么样
  • Redis如何做持久化的
  • 如果突然机器掉电会怎样
  • bgsave的原理是什么
  • Pipeline有什么好处,为什么要用pipeline
  • Redis的同步机制了解么
  • 是否使用过Redis集群,集群的原理是什么?
  • Redis过期策略
  • Redis定期过期策略
  • Redis内存淘汰策略
  • Redis事务了解不
  • 一致性hash算法
相关产品与服务
云数据库 Redis
腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档