Redis

Redis支持哪些数据类型?

Redis支持以下几种数据类型:

  • String(字符串):最基本的数据类型,可以存储字符串、整数或浮点数。
  • Hash(哈希):存储键值对的无序散列表,适合存储对象。
  • List(列表):有序的字符串列表,可以在列表的两端进行插入和删除操作,支持对列表进行修剪和范围查询。
  • Set(集合):无序且唯一的字符串集合,支持对集合进行添加、删除和成员判断等操作。
  • Sorted Set(有序集合):与集合类似,每个成员关联一个分数,根据分数进行排序。支持添加、删除、按分数范围获取成员等操作。
  • Bitmap(位图):可以对位进行设置或获取,常用于记录用户行为、统计等场景。
  • HyperLogLog:使用极小的内存空间对大量的唯一元素进行基数估算,用于统计独立用户数量等。
  • Geospatial(地理位置):存储地理位置信息(经度和纬度),支持查询附近位置、计算距离等操作。

什么是缓存穿透?缓存击穿?缓存雪崩?怎么解决?

缓存穿透

缓存穿透:缓存中查不到,数据库中也查询不到。

解决方案:

  • 对参数进行合法性校验
  • 将数据库中没有查到的结果的数据也写入到缓存,这时要注意为了防止Redis被无用的key占满,这一类缓存的有效期要设置得短一点
  • 引入布隆过滤器,在访问Redis之前判断数据是否存在。要注意布隆过滤器存在一定的误判率,并且,不空过滤器只能加数据不能删数据。

缓存击穿

缓存击穿:缓存中没有,数据库中有,一般是出现在数据初始化以及key过期了的情况,他的问题在于,重新写入缓存需要一定的时间,如果是在高并发场景下,过多的请求就会瞬间写到DB上,给DB造成了很大的压力。

解决方案:

  • 使用二级缓存:在系统架构中引入二级缓存,将热点数据缓存在内存中的一级缓存中,同时使用持久化存储作为二级缓存,当一级缓存失效时,可以从二级缓存中获取数据,避免直接访问后端服务
  • 使用热点数据预加载(Cache Pre-warm):在系统启动或热点数据即将过期之前,提前主动加载热点数据到缓存中,避免在热点数据失效时出现大量请求。通过定时任务或异步线程更新缓存,确保热点数据的实时性
  • 设置热点数据永不过期:对于一些热点数据,可以设置其过期时间为永不过期,或者设置一个相对较长的过期时间,以确保热点数据不会过期失效。但是需要注意,这种方式可能会导致缓存数据与数据库的数据不一致,需要在后端服务更新数据时同时更新缓存
  • 添加互斥锁(Mutex Lock):在缓存失效的情况下,只允许一个请求访问后端服务或数据库,并将结果缓存起来。其他请求在等待该请求的执行结果时,直接从缓存中获取数据,避免对后端服务的重复访问。这种方式可以防止大量请求同时访问后端服务,减轻服务压力
  • 使用布隆过滤器(Bloom Filter):布隆过滤器是一种高效的数据结构,用于判断某个元素是否存在于集合中。可以将热点数据的键存储在布隆过滤器中,当请求到来时,先通过布隆过滤器判断该键是否存在于缓存中,如果不存在,就不再访问后端服务,避免缓存穿透和击穿。

缓存雪崩

缓存雪崩:缓存大面积过期,导致请求都被转发DB。

解决方案:

  • 把缓存的失效时间分散开,例如,在原有的统一失效时间基础上,增加一个随机值。

如何保证Redis与数据库的数据一致?

当我们对数据进行修改的时候,到底是先删缓存,还是先写数据库?

先删缓存,再写数据库

在高并发场景下,当第一个线程删除了缓存,还没有来得及的写数据库,第二个线程来独去数据,会发现缓存中的数据为空,那就会去读数据库中的数据(旧值、脏数据)读完之后,把读到的结果写入缓存(此时,第一个线程已经将新的值写到缓存里面了),这样缓存中的值就会被覆盖为修改前的脏数据。

总结:在这种方式下,通常要求写操作不会太频繁。

解决方案:

  • 先操作缓存,但是不删除缓存,将缓存修改为一个特殊值(-999),客户端读缓存时,发现是默认直,就休眠一小会,再去查一次Redis,特殊值对业务有侵入,可能会多次重复
  • 延时双删,先删除缓存,再写数据库,休眠一小会,再次删除缓存。如果数据写操作很频繁,同样还是会有脏数据的问题。

先写数据库,再删缓存

先写数据库,再删缓存,如果数据库写完了之后,缓存删除失败,数据就会不一致, 总结:始终只能保证一定时间内的最终一致性。

解决方案:

  • 给缓存设置一个过期时间,问题:过期时间内,缓存数据不会更新
  • 引入MQ,保证原子操作。将热点数据缓存设置为永不过期,但是在value当中写入一个逻辑上的过期时间,另起一个后台线程,扫描这些key,对于已逻辑上过期的缓存,进行删除

如何设计一个分布式锁?如何对锁性能进行优化?

分布式锁的本质:就是在所有进程都能访问到的一个地方,设置一个锁资源,让这些进程都来竞争锁的资源,数据库、zookeeper、Redis,通常对于分布式锁,会要求响应快、性能高与业务无关。

Redis实现分布式锁:SETNX key value 当key不存在时,就将key设置为value,并返回1,如果key存在就返回0。EXPIRE key locktime 设置key的有效市场,DEL key 删除。 GETSET key value 先GET,再SET,先返回key对应的值,如果没有就返回空,然后再将key设置成value。

  • 最简单的分布式锁:SETNX 加锁。DEL解锁。问题:如果获取到锁的进程执行失败,他就永远不会主动解锁,那这个锁就被锁死了。
  • 给锁设置过期时长。问题:SETNX和EXPIRE并不是原子性的,所以获取到锁的进程有可能还没有执行EXPIRE指令,就挂了,这时锁还是会被锁死。
  • 将锁的内容设置为过期时间(客户端时间+过期时长),SETNX获取锁失败时,拿这个时间跟当前时间比对,如果是过期的锁,就先删除锁,再重新上锁。问题:在高并发场景下,会产生多个进程同时拿到锁的情况
  • setNX失败后,获取锁上的时间戳,然后用getset,将自己的过期时间更新上去,并获取旧值,如果这个旧值,跟之前获得的时间戳是不一致的,就表示这个锁已经被其他进程占用了,自己要放弃竞争锁。
public boolean tryLock(RedisConnection conn) {
    long newTime = System.currentTimeMillis();
    long expireTime = nowTime + 100;
    if(conn.SETNX("mykey"),"1") == 1) {
        conn.EXPIRE("mykey",1000)
        return true;
    }else {
        long oldVal = conn.get("mykey")
        if(oldVal == null && oldVal < nowTime) {
            long currentVal = conn.GETSET("mykey",expireTime)
            if(oldVal == currentVal) {
                conn.EXPIRE("mykey",1000);
                return true;
            }
            return false;
        }
        return false;
    }
}

上面就形成了一个比较高效的分布式锁。分析一下,上面优化的各种问题,在于SETNX和EXPIRE两个指令无法保证原子性。Redis2.6提供了直接执行lua脚本的方式,通过lua脚本来保证原子性,redission。

Redis的过期删除策略?

redis设置key的过期时间:

$ EXPIRE | SETEX

实现原理:

  • 定期删除:每隔一段时间,执行一次删除过期key的操作,平衡执行效率和执行时长。定期删除会遍历每个database(默认16个),检查当前库中指定个数的key(默认是20个),随机抽查这些key,如果有过期的,就删除。程序中有一个全局变量扫描到了哪个数据库。
  • 懒汉式删除:当使用get、getset等指令去获取数据时,判断key是否过期,过期后,就先把key删除,再执行后面的操作。

Redis是将两种方式结合来使用的。

RDB操作,子进程会全部复制父进程的数据吗?

RDB快照是一次全量备份,当进行快照持久化的时候会开启一个子进程专门负责快照持久化,子进程会拥有父进程的内存数据,父进程修改内存子进程不会反应出来,所以快照持久化期间修改的数据不会保存,存在丢失数据的可能性。

Redis的哨兵模式?

哨兵模式是Redis一种特殊的模式,Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。

Redis使用单线程为什么速度这么快?

严格意义上来说,Redis Server是多线程的,只是它的请求处理整个流程是单线程处理的,我们平常说的Redis单线程快是指它的请求处理非常地块。

Redis每秒可以承受10w+的QPS,它如此优秀的性能主要取决于以下几个方面:

  • 纯内存操作

    所有的数据都存储在内存当中,这意味着读写数据都是在内存中完成,并且使用哈希表的数据结构,只需要O(1)的时间复杂度。

  • 使用IO多路复用技术

    Redis采用IO多路复用计数和非阻塞IO,Redis可以在单线程中监听多个Socket的请求,在任意一个Socket可读/可写时,Redis去读取客户端请求,在内存中操作对应的数据,然后再写回到Socket中。

  • 非CPU密集型任务

    Redis的大部分操作并不是CPU密集型任务,而Redis的瓶颈在于内存和网络带宽,如果单个Redis实例的性能不足以支撑业务,推荐部署多个Redis节点,组成集群的方式来利用多核CPU的能力。

  • 单线程的优势

    没有了线程上下文切换的性能耗损,也没有了访问共享资源加锁的性能损耗,开发和调试非常友好,可维护性高。

Redis自增命令使用?

每当有原子性自增的操作就可以使用INCR命令,主要在计数器场景使用,可以INCR和EXPIRE,来达到规定的生存时间内进行计数的目的,客户端也可以通过使用GETSET命令原子性地获取计数器的当前值并将计数器清零。

使用其他自增/自减操作,比如DECR和INCRBY,用户可以通过执行不同的操作增加或者减少计数器的值。

Redis如何实现消息队列?

Redisson实现分布式锁的原理?

Redis为什么能通过Lua脚本保证并发的线程安全?

谈一下Redis事务的了解?

Redis中事务的实现特征:

  • 在事务中的所有命令都将会被串行化的顺序执行,事务执行期间,Redis不会再为其它客户端的请求提供任何服务,从而保证了事物中的所有命令被原子的执行
  • 和关系型数据库中的事务相比,在Redis事务中如果有某一条命令执行失败,其后的命令仍然会被继续执行
  • 我们可以通过MULTI命令开启一个事务,有关系型数据库开发经验的人可以将其理解为"BEGIN TRANSACTION"语句。在该语句之后执行的命令都将被视为事务之内的操作,最后我们可以通过执行EXEC/DISCARD命令来提交/回滚该事务内的所有操作。这两个Redis命令可被视为等同于关系型数据库中的COMMIT/ROLLBACK语句
  • 在事务开启之前,如果客户端与服务器之间出现通讯故障并导致网络断开,其后所有待执行的语句都将不会被服务器执行。然而如果网络中断事件是发生在客户端执行EXEC命令之后,那么该事务中的所有命令都会被服务器执行
  • 当使用Append-Only模式时,Redis会通过调用系统函数write将该事务内的所有写操作在本次调用中全部写入磁盘。然而如果在写入的过程中出现系统崩溃,如电源故障导致的宕机,那么此时也许只有部分数据被写入到磁盘,而另外一部分数据却已经丢失。Redis服务器会在重新启动时执行一系列必要的一致性检测,一旦发现类似问题,就会立即退出并给出相应的错误提示。此时,我们就要充分利用Redis工具包中提供的redis-check-aof工具,该工具可以帮助我们定位到数据不一致的错误,并将已经写入的部分数据进行回滚。修复之后我们就可以再次重新启动Redis服务器了

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

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

Redis延迟队列怎么实现的?

详细参见:Redis延迟队列open in new window

Redis在内存不足时,内存淘汰策略是怎么样的?

以下是Redis的内存淘汰策略:

  • noeviction:默认策略,当内存不足以容纳新写入数据时,新写入操作会报错并返回错误信息
  • allkeys-lru:Least Recently Used (LRU) 最近最少使用策略,从所有的键中选择最近最少使用的键进行删除
  • volatile-lru:从设置了过期时间的键中选择最近最少使用的键进行删除
  • allkeys-random:随机选择要删除的键
  • volatile-random:从设置了过期时间的键中随机选择要删除的键
  • volatile-ttl:根据键的剩余过期时间来选择删除键,越早过期的键优先删除
  • volatile-lfu:Least Frequently Used (LFU) 最不经常使用策略,从设置了过期时间的键中选择使用频率最低的键进行删除

如何保证Redis的高可用?