当前位置: 首页 > 科技观察

腾讯采访的话浅浅深浅,Redis

时间:2023-03-19 14:54:05 科技观察

本文转载自微信公众号《码农的私房话》,作者Liew。转载本文请联系码农包间公众号。redis和memcached数据类型的区别:memcached只支持string,而redis还支持list列表、集合set、hash、有序集合sortedSet、位图bitMap、HyperLogLog和Stream(redis5.0版本新特性)。主从备份:redis支持主从模式应用,主库多副本,可以扩展数据库读取能力和高可用。数据存储:memcached和redis都支持数据在内存中存储。此外,redis还支持将数据保存在磁盘中。持久化:memcached的数据只保存在内存中,宕机后数据会丢失,而redis可以利用持久化机制将数据保存在磁盘上,便于归档或恢复。事务:redis支持事务,可以原子地执行一组命令。Pub/Sub:Redis支持具有模式匹配的Pub/Sub消息传递。Lua脚本:redis允许执行事务性Lua脚本,有助于提高性能并简化应用程序代码。地理空间支持:redis有专门的命令可以处理大规模的实时地理空间数据,例如查找两个元素(人或地点)之间的距离,查找一个点给定距离内的所有元素。线程模型:redis采用的是单线程模型,而memcached采用的是多线程架构,可以使用多核,扩展计算能力来处理更多的操作,性能会在一定程度上优于redis。多语言客户端支持:redis和memcached都支持多语言客户端,包括Java、Python、Php、C、C++、Go等。redis有哪些数据类型StringString、list列表、集合set、有序集合sortedSet、hash哈希、位图bitMap、Stream流(redis5.0版本新特性)、HyperLogLog。redis的使用场景有哪些?热点数据缓存:可以将系统中常用的、不经常更新的数据加载到redis中,提高性能。分布式锁:结合setexnx命令或直接使用Redisson功能。排行榜:使用SortedSets轻松实现游戏排行榜。队列:redislist底层是一个链表,但是也可以用来做队列。使用lpush和brpop命令来操作队列。计数器:控制一个手机号码每天限制发送5条短信,或者用来抵扣库存,以免超发。布隆过滤器:快速准确判断10万个号码是否在10亿号码库中,或者请求的IP地址是否在10亿黑名单库中。GeoHash:实现美团外卖或饿了么的“附近商户”功能,或者计算两个人之间的距离。BitMap:Bitmap可以用来实现用户最近7天的类似登录状态或者某个时间范围内用户的登录状态。延时操作:使用SortSet实现延时队列,比如订单30分钟内未付款,则自动取消并发送短信。好友关系和点赞数:集合集可用于记录文章的点赞数和阅读量;使用zinterstore查询共同好友,使用zset实现好友关系。分布式限流:基于令牌桶算法,使用Lua脚本实现分布式限流。SpringCloudGateway中的限流就是一个典型的例子。redis线程模型在redis6版本之前采用单线程模型,基于Reactor模型开发了网络事件处理器。Redis在处理客户端的请求时,包括请求命令的获取、解析、执行、内容返回,都是由一个Sequential串行主线程控制的,这就是所谓的“单线程”。不过多线程模型是在redis6版本之后才正式引入的。随着业务场景越来越复杂,需要更高的QPS。常见的方案是对数据进行分区,使用更多的服务器,但这种方案的缺点是投资成本高,需要维护的redis服务器较多。redis执行过程中,大部分CPU时间被网络读写和系统调用占用,瓶颈主要在于网络的IO消耗。因此,优化的主要原因如下:充分利用服务器CPU资源,而当前redis主线程只能使用一个核。多线程任务可以分担redis同步IO读写负载,例如:memcached。为什么redis选择单线程网络模型?使用单线程模型可以带来更好的可维护性,方便开发和调试。也可以使用单线程模型并发处理客户端请求。避免多线程带来的频繁CPU上下文切换开销和安全同步。Redis服务中运行的绝大多数操作的性能瓶颈并不是CPU。从官方给出的分析可以看出,官方认为redis大部分命令操作的性能瓶颈不在CPU,而主要受限于内存和网络。CPU成为Redis瓶颈的情况并不常见,因为Redis通常受内存或网络限制。例如,使用在普通Linux系统上运行的流水线Redis每秒甚至可以传递100万个请求,因此如果您的应用程序主要使用O(N)或O(log(N))命令,则几乎不会占用太多CPU.所以第三点起到了决定性的作用,另外两点就是使用单线程的好处。为什么redis这么“快”?1、完全基于内存,大部分请求都是纯内存操作。2.使用单线程避免了不必要的上下文切换和竞争条件,但同时也无法发挥多核的优势。3、使用多通道I/O复用模型,实现高吞吐量IO操作。4、数据结构简单,大部分读写操作都是O(n)或O(log(N))。队列使用redis有什么问题?1.存在消息丢失的可能。2.生产速度和消费速度不匹配会造成消息堆积,导致redis内存耗尽。3、队列中的消息不允许重复消费。Redis是如何实现分布式锁的?实现思路大致如下:1.使用setnx和setex命令保存当前请求信息(lockkey,threadid等(可重入函数))。2.在释放锁的时候,为了避免误删除,需要判断当前运行的线程是否和加锁的相同,如果相同则del对应锁的key。涉及到多条命令的执行,需要将获取锁和释放锁的逻辑放在lua脚本中,保证原子性。具体可以参考Redisson分布式锁的实现:锁获取逻辑代码:if(redis.call('exists',KEYS[1])==0)thenredis.call('hset',KEYS[1],ARGV[2],1);redis.call('pexpire',KEYS[1],ARGV[1]);returnnil;end;if(redis.call('hexists',KEYS[1],ARGV[2])==1)thenredis.call('hincrby',KEYS[1],ARGV[2],1);redis.call('pexpire',KEYS[1],ARGV[1]);returnil;end;returnredis.call('pttl',KEYS[1]);释放锁逻辑代码:if(redis.call('hexists',KEYS[1],ARGV[3])==0)thenreturnnil;end;localcounter=redis.call('hincrby',KEYS[1],ARGV[3],-1);if(counter>0)thenredis.call('pexpire',KEYS[1],ARGV[2]);return0;elseredis.call('del',KEYS[1]);redis.call('publish',KEYS[2],ARGV[1]);return1;end;returnnil;如何解决业务端逻辑还没执行完,锁已经过期的问题?可以参考Redisson框架对锁租期的处理方案,每次获取锁时判断leaseTime是否为-1,如果是,则获取锁成功后,新建线程检测是否锁对应的key过期,如果过期,调用lua脚本更新key的过期时间。privateRFuturetryAcquireOnceAsync(longleaseTime,TimeUnitunit,longthreadId){if(leaseTime!=-1){returntryLockInnerAsync(leaseTime,unit,threadId,RedisCommands.EVAL_NULL_BOOLEAN);}RFuturettlRemainingFuture=tryLockInnerAsync(commandExecutor.getConnectionManager()。getCfg().getLockWatchdogTimeout(),TimeUnit.MILLISECONDS,threadId,RedisCommands.EVAL_NULL_BOOLEAN);ttlRemainingFuture.onComplete((ttlRemaining,e)->{if(e!=null){return;}//lockacquiredif(ttlRemaining){scheduleExpirationRenewal(threadId);}});returnttlRemainingFuture;}scheduleExpirationRenewal()方法具体使用如下:privatevoidrenewExpiration(){ExpirationEntryee=EXPIRATION_RENEWAL_MAP.get(getEntryName());if(ee==null){return;}Timeouttask=commandExecutor.getConnectionManager().newTimeout(newTimerTask(){@Overridepublicvoidrun(Timeouttimeout)throwsException{ExpirationEntryent=EXPIRATION_RENEWAL_MAP.get(getEntryName());if(ent==null){return;}LongthreadId=ent.getFirstThreadId();if(threadId==null){return;}RFuturefuture=renewExpirationAsync(threadId);future.onComplete((res,e)->{if(e!=null){log.error("Can'tupdatelock"+getName()+"expiration",e);return;}//rescheduleitselfrenewExpiration();});}},internalLockLeaseTime/3,TimeUnit.MILLISECONDS);ee.setTimeout(task);}通过代码发现每次锁更新后租期结束,会重新创建一个新的线程来刷新租期。redislua实现分布式锁的问题1.对于设置了租约时间的客户端,长期阻塞会导致锁失效。2、当redismaster故障时,一个slave升级为新的master,但是锁信息没有同步到新的master,导致其他请求获取锁。什么是红锁?当redis主从发生漂移时,会导致锁失效。redis的作者提出了RedLock算法:假设redis的部署方式是rediscluster,一共有3个master节点。锁定时,它将发送到大多数节点。发送setexmykeymyvalue命令,只要超过一半的节点成功,就认为加锁成功。同样,释放锁时,需要向所有节点发送del命令。有兴趣的可以阅读Redisson源码实现。使用RedLock虽然解决了master故障导致的同步问题,但是需要更多的redis实例资源,同时性能也会有一定的打折扣。说说缓存穿透、击穿、雪崩缓存穿透:指不在缓存或数据库中的数据,用户或攻击者不断发起请求,如userId为负数的数据或不存在的数据,会造成过大数据库压力大,解决方法:1.检查参数值的合法性,用户认证等2.对于缓存或数据库中不存在的数据,可以将其userId->null映射到缓存中,并设置根据业务场景设置过期时间,防止攻击者暴力攻击。缓存击穿:指缓存中没有数据,但数据库中有数据。一般是没有做热加载或者缓存过期导致的。某一时刻,由于对同一条数据的大量并发查询请求,读缓存中没有数据,所以同时去数据库。查询数据,导致数据库压力瞬间增大。解决方法:1、将热点数据缓存的过期时间设置的长一些或者永久的。2、当查询缓存中没有数据时,使用互斥控制,只允许一个线程A查询数据库,其余请求线程等待线程A加载数据到缓存中。比如当GuavaCache查询缓存中没有数据时,只允许一个线程加载。缓存雪崩:指缓存中大量数据的过期时间,同时查询数据量巨大,造成数据库压力过大甚至宕机。与缓存击穿不同的是,缓存击穿是围绕并发查询同一块数据,而缓存雪崩是指不同的数据都过期了,很多数据查不到数据库。解决方法:1.随机设置缓存数据的过期时间,防止大量数据同时过期。2.设置热点数据过期时间可以长一些或者永久。谈谈缓存和数据库的一致性问题常见的缓存和数据库操作顺序有几种方式:先写缓存,再更新DB:如果第一步更新缓存失败,直接返回,没有任何影响。如果缓存写入成功,则更新数据库失败。这时候如果不清除缓存中写入的数据,就会导致数据不一致(缓存中有新值,DB中有旧值)。如果增加了清除缓存的逻辑,如果清除操作失败怎么办?先更新DB,再写入缓存:如果DB更新失败,直接返回,无任何影响。如果DB的更新成功,写入缓存失败会导致数据不一致(即DB中的新值和缓存中的旧值)。如果重试写入缓存,重试失败怎么办?UpdateDB:如果删除缓存失败,直接返回,无任何影响。如果缓存删除成功,但DB更新失败,后续请求会错过缓存,数据会从数据库中获取。先更新DB,再删除缓存:如果DB更新失败,直接返回,没有任何影响。如果DB更新成功,删除缓存失败会导致数据不一致(DB中的新值和缓存中的旧值)。这个问题本质上是一个分布式数据一致性问题。在不需要强一致性的场景下,可以保证最终一致性。更新数据库后,通过Canal订阅MySQL的binlog日志,使缓存失效。如果缓存操作失败,将缓存的信息放到MQ中重试。如果数据需要强一致性或者不能接收脏数据,最简单的方法就是直接上数据库,不使用缓存。redis的持久化方式有哪些?RDB,全称RedisDatabase,在指定的时间间隔内,将内存中的数据集作为快照写入磁盘。实际操作过程是fork一个子进程,先将数据集写入一个临时文件,写入成功后替换之前的文件,二进制压缩存储,恢复数据时将快照文件直接读入内存.优点:RDB快照是压缩后的二进制文件,文件体积小,比较适合全量复制备份的场景。与AOF机制相比,如果数据集很大,RDB的恢复效率会更高。缺点:如果要保证数据的高可用,也就是最大程度的避免数据丢失,那么RDB并不是一个好的选择,因为一旦在预定的持久化之前系统宕机,还没有写入的数据磁盘及时会丢失。因为每次生成RDB快照,都需要fork子进程生成全量数据的快照,占用CPU和磁盘资源,不适合频繁执行。兼容性问题,不同版本的redis生成的快照可能不兼容。AOF,全称AppendOnlyFile,将操作命令和数据以格式化的方式追加到操作日志文件的末尾。追加操作返回后(已经写入文件或即将写入),进行实际的数据更改。日志文件保存所有历史操作。当redis服务器需要恢复数据时,可以直接重放日志文件来恢复所有操作。在redis中,有三种同步策略:每秒同步、每修改同步、不同步。真正的二次同步是异步完成的,效率高。一旦系统宕机,这一秒内修改的数据就会丢失。每次同步修改,即发生的每一次数据变化都会立即记录到磁盘中。可以想象,这种同步方式效率最低。优点:AOP机制提供了更高的数据安全性,即数据持久化。AOF持久化方式包含了一份格式清晰、通俗易懂的日志内容,用于记录所有的修改操作。对于数据写入一半后系统崩溃的现象,redis可以通过redis-check-aof工具帮助解决数据一致性问题。当日志文件过大时,redis会启动rewrite机制,可以删除部分命令。缺点:同样的数据量,AOF文件通常比RDB文件大,AOF的恢复速度低于RDB。根据同步策略的不同,AOF在运行效率上通常比RDB慢,但是每秒同步策略的效率是比较高的,禁用同步策略的效率和RDB差不多。至于选择哪种持久化方式,取决于系统能否接受一些性能牺牲,是使用AOF方式换取更高的数据一致性,还是禁用RDB备份换取更高的性能,等待请求量大的时间点或流量低。定时执行save命令进行快照备份,但目前生产环境多是两者结合使用。redis部署的单机模式有哪些,即只有一个redis实例,所有的服务都连接到这个实例上。这种模式不适合生产环境。如果redis实例宕机或者内存不足,所有的服务都会受到影响。Sentinel模式,redis官方推荐的高可用方案,master宕机后,redis本身不具备自动主备切换的功能,redis-sentinel是一个独立运行的进程,可以监控多个master-slaveclusters和findmaster宕机后,可以自动选举出新的master。集群模式,随着业务和数据量的快速增长,已经达到单节点的性能瓶颈,纵向扩展受限于机器,横向扩展涉及对业务的影响,数据迁移存在数据丢失的风险.因此在redis3.0推出了cluster分布式集群方案。当遇到单节点内存、并发、流量瓶颈时,可以采用集群方案来实现负载均衡。该方案主要解决分片问题,将整个数据按照规则划分为多个子集进行存储。在多个不同的redis节点上,每个节点负责整个数据的一部分。为什么redis使用哈希槽而不是一致性哈希?redis集群不直接使用一致性哈希,而是使用哈希槽。区别在于散列空间的定义。consistenthashing的空间是一个环,节点的分布是基于环的,无法很好的控制数据的分布,可能会造成数据倾斜的问题。但是redis的slot空间是自定义分配的,可以自定义大小和位置。redis集群包含16384个hashslot,每个Key经过CRC16算法计算后会落入特定的slot,slot所在的具体机器由用户根据机器情况配置,以及机器的硬盘很小。硬盘可以少分配槽位,硬盘可以多分配槽位。此外,在容错性和可扩展性方面,它与一致性哈希相同,传输受影响的数据。哈希槽本质上是槽的转移,将故障节点负责的槽转移到其他正常节点,扩容节点也是如此,将其他节点上的槽转移到新节点。说说数据迁移过程中客户端访问数据的过程。在数据迁移过程中,一些数据存在于新旧节点对应的槽中。客户端首先尝试访问旧节点。如果对应的数据在老节点,老节点正常处理。.如果不在旧节点上,它可能在新节点上或不存在。当客户端访问不存在的旧节点时,会向客户端返回一个ASK或MOVED重定向命令,其中MOVED是一个永久的转向信号,ASK表示转向只需要这个操作。需要注意的是,客户端在查询新节点时,需要先发送ASKING命令,否则请求命令会被状态为IMPORTING的新槽节点拒绝。对于客户端来说,当收到MOVED时,需要更新slot映射信息,当收到ASK时,需要向新的节点发送ASKING命令,重新执行操作命令。Redis过期数据清除机制被动删除:当操作读写过期key时,会触发惰性删除策略,直接删除过期key并返回NIL。主动删除:由于惰性删除策略不能保证及时删除冷数据,redis会定期主动淘汰清除过期键。Redis内存清除策略当当前使用的内存超过redis配置的maxmemory限制时,会触发主动清理策略。策略如下:noeviction:不进行数据淘汰。当缓存满了,Redis将不提供服务,直接返回错误。volatile-random:随机删除设置过期时间的键值对。volatile-ttl:设置过期时间的键值对按照过期时间的先后顺序删除。过期时间越早,删除越早。volatile-lru:基于LRU(LeastRecentlyUsed)算法对设置了过期时间的键值对进行过滤,根据最近最少使用原则过滤数据。volatile-lfu:使用LFU(LeastFrequentlyUsed)算法选择设置了过期时间的键值对,使用频率最少的原则过滤数据allkeys-random:从所有键值对中随机选择和删除数据.allkeys-lru:使用LRU算法筛选所有数据。allkeys-lfu:使用LFU算法过滤所有数据。在线redis实例内存不足。如何应对,这是腾讯音乐采访时提出的问题。考察应急问题处理能力,第一要素是解决问题,即在线扩容,不能影响用户功能的使用,但在线扩容恰恰是急需。数据增加后无法继续扩展。毕竟,成本摆在那里。因此,可以采用redis集群方案,将数据均衡分布存储在不同的redis实例中,解决redis单实例存储过大的问题,但是使用redis集群方案也会引入一定的问题,比如某些命令无法执行在集群下执行,增加数据迁移的复杂度等。