将常用的SQL数据库的数据存储在磁盘上,虽然在数据库底层也做了相应的缓存,以减轻数据库的IO压力。图片来自Pexels。因为数据库缓存一般是针对查询内容,而且粒度比较小,一般只有当表中的数据没有发生变化时,数据库缓存才会生效。但是,这并没有减轻业务逻辑对数据库增删改查的IO压力。因此,缓存技术应运而生。该技术实现了热点数据的缓存,可以大大缓解后端数据库的压力。主流应用架构客户端在向数据库发起请求时,首先会去缓存层检查是否有需要的数据。如果缓存层包含客户端需要的数据,则直接从缓存层返回;查询数据库。如果在数据库中查询数据,会将数据写回到缓存层,这样下次客户端再次查询时,可以直接从缓存层获取数据。缓存中间件Memcache和Redis的区别Memcache代码层类似于Hash,具有以下特点:支持简单数据类型不支持数据持久化存储不支持主从不支持分片Redis特点如下:丰富数据类型支持数据盘持久化存储支持Master-slave支持sharding为什么Redis可以这么快Redis效率很高,官方数据是100000+QPS,这是因为:Redis完全基于内存,大部分请求都是纯内存操作,执行效率高。Redis采用单进程单线程模型(K、V)数据库,数据存储在内存中,访问不受硬盘IO限制,因此执行速度极快。另外,单线程还可以处理高并发请求,避免频繁的上下文切换和锁竞争。如果要多核运行,也可以启动多个实例。数据结构简单,数据操作也简单。Redis不使用表,不强制用户关联每一个关系,没有复杂的关系限制。它的存储结构是键值对,类似于HashMap。HashMap最大的优点就是访问的时间复杂度是O(1)。Redis采用了multiplexedI/O多路复用模型,也就是非阻塞IO。注:Redis使用的I/O多路复用函数:epoll/kqueue/evport/select。选型策略:因地制宜,优先选择时间复杂度为O(1)的I/O复用函数作为底层实现。由于Select需要遍历每一个IO,所以其时间复杂度为O(n),通常作为保底方案使用。基于React设计模式监听I/O事件。Redis的数据类型String是最基本的数据类型,它的值最大可以存储512M,而且是二进制安全的(Redis的String可以包含任何二进制数据,包括jpg对象等)。注意:如果重复写入相同的键值对,后面的写入会覆盖前面的写入。HashString元素的字典,适用于存储对象。List列表,按照String元素的插入顺序排序。顺序是后进先出。因为它具有堆栈的特性,所以可以实现“最新消息排行榜”等功能。一个由SetString元素组成的无序集合,通过哈希表实现(增删改查的时间复杂度为O(1)),不允许重复。另外,当我们使用Smembers遍历Set中的元素时,顺序也是不确定的,这是Hash运算的结果。Redis还提供了集合的交集、并集、差集等操作,可以实现相互关注、相互好友等功能。SortedSet使用分数将集合的成员从小到大排序。Redis比较高级的类型有用于计数的HyperLogLog,用于支持存储地理位置信息的Geo。从大量的Key中找出一个前缀固定的Key假设Redis中有十亿个Key,如何从这么多的Key中找出一个前缀固定的Key?方法一:使用Keys[pattern]:查找所有符合给定pattern的key时间,所以会导致Redis卡死。假设此时Redis处于生产环境,使用该命令会存在隐患。另外,如果一次返回所有key,在某些情况下内存消耗会很大。例子:keystest*//返回所有以test为前缀的键方法二:使用SCAN游标[MATCHpattern][COUNTcount]注:cursor:游标MATCHpattern:queryKeyconditionCount:返回项的个数SCAN是基于迭代器cursor需要在上一个cursor的基础上继续上一个迭代过程。SCAN从游标0开始新的迭代,直到命令返回游标0以完成遍历。该命令不保证每次执行都会返回给定数量的元素,甚至会返回0个元素,但只要光标不为0,程序就不会认为SCAN命令结束,而是返回的元素数量很可能满足Count参数。此外,SCAN支持模糊查询。例子:SCAN0MATCHtest*COUNT10//每次返回10个前缀为test的key如何通过Redis实现分布式锁分布式锁是一种控制分布式系统间共享资源的锁实现。如果一个系统或不同系统的不同主机共享某种资源,往往需要互斥来消除干扰,满足数据的一致性。分布式锁需要解决的问题如下:互斥:任何时候只能有一个客户端获取锁,不能有两个客户端同时获取锁。安全性:锁只能被持有锁的客户端删除,其他客户端不能删除。死锁:获取锁的客户端由于某种原因宕机,无法释放锁,其他客户端也无法再获取锁,从而导致死锁。这时候就需要一种特殊的机制来避免死锁。容错性:当每个节点,如Redis节点宕机时,客户端仍然可以获取或释放锁。如何使用Redis实现分布式锁使用SETNX实现,SETNX键值:如果Key不存在,则创建并赋值。这条命令的时间复杂度为O(1),如果设置成功则返回1,否则返回0。由于SETNX指令简单且原子性强,所以早期常被用作分布式锁。我们在应用的时候,可以在一个共享资源区之前使用SETNX指令来检查是否设置成功。如果设置成功,说明没有前面的客户端在访问该资源。如果设置失败,说明有客户端访问该资源,则当前客户端需要等待。但是如果真的这样做了,就会出现问题,因为SETNX长期存在,所以如果一个client在访问资源的时候加锁,那么当client结束访问的时候,锁还存在,后面就无法成功获取锁,如何解决?由于SETNX不支持传入EXPIRE参数,我们可以直接使用EXPIRE命令为具体的Key设置过期时间。用法:EXPIREkeyseconds程序:RedisServiceredisService=SpringUtils.getBean(RedisService.class);longstatus=redisService.setnx(key,"1");if(status==1){redisService.expire(key,expire);doOcuppiedWork();}这个程序的问题:假设程序运行到第二行发生了异常,那么程序在设置过期时间之前就结束了,Key会一直存在,相当于锁被持有,无法释放.这个问题的根本原因是:不满足原子性。解决方案:从Redis2.6.12版本开始,我们可以使用Set操作结合SETNX和EXPIRE,具体方法如下:EX秒:设置key的过期时间为Second秒。PX毫秒:设置密钥的过期时间为MilliSecond毫秒。NX:仅当密钥不存在时才设置密钥。XX:仅当密钥已存在时才设置密钥。SETKEYvalue[EXseconds][PXmilliseconds][NX|XX]注意:只有成功完成SET操作才会返回OK,否则返回nil。有了SET,我们就可以在程序中使用类似下面的代码来实现分布式锁:OK.equals(result)"){doOcuppiredWork();}如何实现异步队列①使用Redis中的List作为队列使用上面提到的Redis数据结构中的List作为队列Rpush产生消息,LPOP消费消息。此时我们可以看到队列使用了Rpush生产队列和LPOP消费队列。在这个生产者消费者队列中,当LPOP没有消息时,证明队列中没有元素,生产者还没有来得及生产新的数据。缺点:LPOP在消费前不等待队列中的值,而是直接消费。解决方法:可以在应用层引入Sleep机制,调用LPOP重试。②使用BLPOPkey[key…]timeoutBLPOPkey[key…]timeout:阻塞直到队列中有消息或者超时。缺点:按照这种方式,我们生产的数据只能提供给每个单独的消费者消费。实现生产后是否可以消费多个消费者?③Pub/Sub:主题订阅者模式发送者(Pub)发送消息,订阅者(Sub)接收消息。订阅者可以订阅任意数量的频道。Pub/Sub模式的缺点:消息的发布是无状态的,无法保证可达性。对于发布者来说,消息是“火了就输了”。此时如果生产者发布消息时消费者下线,再次上线后就收不到消息了。要解决这个问题,就需要专业的消息队列,比如Kafka……这里不再赘述。RedisPersistence什么是持久化持久化是指在不因断电或其他复杂的外部环境影响数据完整性的情况下,持久化存储数据。由于Redis将数据存储在内存中而不是磁盘中,一旦内存断电,Redis中存储的数据会立即消失,这往往是用户所不希望的,因此Redis有持久化机制来保证数据安全。Redis如何持久化Redis目前有两种持久化方式,分别是RDB和AOF。RDB通过保存某个时间点的全量数据快照来实现数据持久化。恢复数据时,直接通过RDB文件中的快照,恢复数据。RDB(快照)持久化RDB持久化会按照特定的时间间隔保存该时间点的全量数据快照。RDB配置文件,redis.conf:save9001#900s内写入1条数据,则生成快照。save30010#300s内写入10条数据,生成快照save6010000#60s内写入10000条数据,生成快照stop-writes-on-bgsave-erroryes#stop-writes-on-bgsave-error:如果是yes,表示当备份进程失败时,主进程将停止接受新的写操作,这是为了保护持久化数据的一致性。①RDB的创建和加载SAVE:阻塞Redis服务器进程,直到RDB文件创建完成。SAVE命令很少使用,因为它会阻塞主线程以确保写入快照。由于Redis使用一个主线程来接收所有的客户端请求,它会阻塞所有的客户端请求。BGSAVE:此命令将派生一个子进程来创建RDB文件,而不会阻塞服务器进程。子进程接收请求并创建RDB快照,父进程继续接收客户端的请求。当子进程创建完文件后,会向父进程发送一个信号,父进程在接收客户端请求的过程中,每隔一定的时间间隔轮询一次,就会收到子进程发来的信号。我们还可以使用lastsave命令来查看BGSAVE是否执行成功,lastsave可以返回上一次BGSAVE执行成功的时间。②自动触发RDB持久化的方式自动触发RDB持久化的方式如下:根据redis.conf配置中的SAVEmn,定时触发(实际使用的是BGSAVE)。主从复制时,自动触发主节点。执行调试重新加载。执行Shutdown,不开启AOF持久化。③BGSAVE的原理开始:检查是否有子进程在执行AOF或者RDB的持久化任务。如果有则返回false。调用Redis源码中的rdbSaveBackground方法,在方法中执行fork()生成子进程进行RDB操作。关于fork()中的写时复制。fork()在Linux中使用Copy-On-Write(写时复制技术)创建子进程,即如果多个调用者同时需要相同的资源(如内存或磁盘上的数据存储)。它们会共同获取指向同一个资源的同一个指针,直到有调用者试图修改资源的内容,系统实际上会复制一份专用的副本给调用者,其他调用者看到的原始资源保持不变。④RDB持久化方式的缺点RDB持久化方式的缺点如下:内存数据完全同步。在数据量很大的情况下,I/O会严重影响性能。从当前到最近的快照期间的数据可能会因Redis宕机而丢失。AOF持久化:保存写状态AOF持久化通过保存Redis的写状态来记录数据库。相对于RDB,RDB持久化是通过备份数据库的状态来记录数据库的,而AOF持久化是指备份数据库收到的指令:AOF记录了除了查询之外的所有改变数据库状态的指令。增量保存到AOF文件。启用AOF持久化①打开redis.conf配置文件,修改appendonly属性为yes。②修改appendfsync属性,可以接收三个参数,分别是always、everysec、no。always表示缓冲区内容总是立即写入AOF文件,everysec表示每秒将缓冲区内容写入AOF文件,no表示写文件的操作交给操作系统。一般来说,考虑到效率问题,操作系统会等待缓冲区填满,然后再将缓冲区数据写入AOF文件。appendonlyyes#appendsyncalwaysappendfsynceverysec#appendfsyncnolog重写解决AOF文件增加的问题随着写操作的不断增加,AOF文件会越来越大。假设一个计数器自增100次,如果使用RDB持久化方式,我们只需要保存最后的结果100即可。AOF持久化方式需要记录这100次自增操作的指令。事实上,要恢复这条记录,只需要执行一条命令,那么一百条命令其实可以缩减为一条。Redis支持这样的功能,可以在不中断前台服务的情况下重写AOF文件,并且还使用了COW(copy-on-write)。重写过程如下:调用fork()创建子进程。子进程将新的AOF写入一个临时文件,独立于原来的AOF文件。主进程继续向内存和原来的AOF同时写入新的变化。主进程获取子进程重写AOF的完成信号,将增量变化同步到新的AOF。用新的AOF文件替换旧的AOF文件。AOF和RDB的优缺点AOF和RDB的优缺点如下:RDB的优点:全数据快照,文件体积小,恢复快。RDB的缺点:不能保存最新快照之后的数据。AOF优点:可读性高,适合保存增量数据,数据不易丢失。AOF的缺点:文件体积大,恢复时间长。RDB-AOF混合持久化方式这种持久化方式是Redis4.0之后引入的。RDB作为全量备份,AOF作为增量备份,默认采用这种方式。以上两种方式中,RDB方式是将全量数据写入RDB文件,特点是文件小,恢复快,但不能保存最新快照后的数据。AOF将Redis命令保存到文件中。这会造成文件体积大、恢复时间长等弱点。RDB-AOF模式下,持久化策略是先将缓存中的所有数据以RDB模式写入文件,然后在AOF模式下追加写入RDB数据后新增的数据,然后进行RDB下一步持久化时间。是时候将AOF数据以RDB的形式重写到文件中了。这种方式不仅可以提高读写和恢复的效率,还可以减小文件的体积,同时保证数据的完整性。在该策略的持久化过程中,子进程会通过管道从父进程读取增量数据。当以RDB格式保存全量数据时,也会通过管道读取数据,不会造成管道堵塞。可以说这种方式的持久化文件前半部分是RDB格式的全量数据,后半部分是AOF格式的增量数据。目前推荐使用这种方式作为持久化方式。RDB和AOF文件共存时Redis数据恢复的恢复流程如下:从图中可以看出,Redis在启动时,会先检查AOF是否存在。如果AOF存在,则直接加载AOF。如果AOF不存在,则直接加载RDB文件。PinelinePipeline类似于Linux的管道,可以让Redis批量执行指令。Redis基于请求/响应模型,单个请求需要一个一个的响应。如果需要同时执行大量命令,每条命令都需要等待上一条命令执行完毕后才能继续执行,不仅增加了RTT,还会多次使用系统IO。由于Pipeline可以批量执行指令,因此可以节省多次IO和请求响应往返的时间。但如果指令之间存在依赖关系,建议分批发送指令。Redis同步机制Master-slave同步原理Redis一般使用一个Master节点进行写操作,若干个Slave节点进行读操作,Master和Slave代表不同的RedisServer实例。另外,定期的数据备份操作也是单独选择一个Slave来完成,这样可以最大限度地发挥Redis的性能,以保证数据的弱一致性和最终一致性。另外Master和Slave的数据不一定要马上同步,而是Master和Slave的数据往往会在一段时间后同步,这就是最终的一致性。完整的同步过程如下:Slave向Master发送Sync命令。Master启动后台进程,将Redis中的数据快照保存到文件中。master在保存数据快照的同时缓存接收到的写命令。master写完文件后,将文件发送给slave。用新的AOF文件替换旧的AOF文件。Master将这段时间收集到的增量写入命令发送给Slave端。增量同步过程如下:Master接收到用户的操作命令,判断是否需要传播给Slave。将操作记录追加到AOF文件中。PropagateoperationstootherSlaves:对齐主从库;将指令写入响应缓存。将缓存中的数据发送给Slave。Redis哨兵(sentinel)主从模式的缺点:当Master宕机时,Redis集群将无法对外提供写操作。RedisSentinel解决了这个问题。解决主从同步后主从切换问题Masterdown:监控:检查主从服务器是否正常运行。警报:通过API向管理员或其他应用程序发送失败通知。自动故障转移:主从切换(master宕机后,其中一个slave转为master,其他slave从该节点同步数据)。Redis集群如何从海量数据中快速找到自己需要的?①分片将数据按照一定的规则划分,分散存储在多个节点上。通过将数据拆分到多个Redis服务器来减轻单个Redis服务器的压力。②一致性哈希算法由于需要对数据进行分片,通常的做法是获取节点的哈希值,然后根据节点个数计算取模。但这种方法有明显的缺点。当需要动态增减Redis节点数量时,会导致大量Key命中失败。因此在Redis中引入了一致性Hash算法。该算法对2^32取模,将Hash值空间形成一个虚拟环。整个环按顺时针方向组织,每个节点依次为0,1,2...2^32-1。然后对每台服务器进行哈希计算,确定服务器在哈希环上的地址。确定服务器地址后,对数据使用相同的Hash算法,将数据定位到特定的Redis服务器上。如果定位到的位置没有Redis服务器实例,则继续顺时针查找,找到的第一个服务器就是数据的最终服务器位置。③Hash环的数据倾斜问题当Hash环中的服务器节点较少时,很容易遇到服务器节点不均匀的问题,从而造成数据倾斜。数据倾斜意味着大部分缓存对象都集中在其中一个Redis集群中。在一台或多台服务器上。如上图所示,一致性哈希算法计算出的大部分数据都存储在节点A上,而只有少量数据存储在节点B上,随着时间的推移,节点A会爆炸。为了解决这个问题,可以引入虚拟节点。简单的说就是为每个服务器节点计算多个哈希值,在每个计算结果位置放置一个服务器节点,称为一个虚拟节点,可以通过在服务器IP或主机名后面放置一个数字来实现。例如上图中:将NodeA和NodeB划分为NodeA#1-A#3和NodeB#1-B#3。结语这篇文章准备(tou)了好久,因为总觉得有些事情拿不定主意,不敢写出来,近乎自闭症。如果有同学觉得哪里不对,欢迎在评论区留言。
