简介Redis是一个高性能的分布式键值数据库。支持多种数据结构,可应用于缓存、队列等多种场景。用过Redis的朋友可能对这些已经很熟悉了。接下来,我想说说Redis可能不是大家所知道的。Redis持久化机制刚看到标题,你可能会说,我知道,无非就是RDB和AOF。这些已经是家常便饭了。那么今天就来深入聊一聊这两种持久化方式的逻辑和原理。RDB的原理在Redis中,RDB持久化触发器分为两种:手动触发和Redis定时触发。对于RDB持久化,可以采用手动触发:(1)save:会阻塞当前Redis服务器,直到持久化完成,应该禁止在线。(2)bgsave:这个触发方法会fork一个子进程,子进程负责持久化进程,所以只有fork子进程时才会发生阻塞。自动触发场景如下:根据我们savemn配置规则自动触发;当slave节点复制完成后,master节点将rdb文件发送给slave节点完成复制操作,master节点会触发bgsave;当执行debugreload时,会被惩罚;执行shutdown时,如果aof没有开启,也会被触发。既然基本不用save,那我们就来看看bgsave命令是如何完成RDB持久化的。RDB文件保存过程(一)redis调用fork,现在有子进程和父进程。(2)父进程继续处理客户端请求,子进程负责将内存内容写入临时文件。由于os的写时复制机制(copyonwrite),父子进程会共享同一个物理页。当父进程处理写请求时,os会创建父进程要修改的页面的副本,而不是写入共享页面。因此,子进程地址空间中的数据是整个数据库在fork时刻的一个快照。(3)子进程将快照写入临时文件完成后,将原来的快照文件替换为临时文件,子进程退出。PS:fork操作会阻塞,导致Redis读写性能下降。我们可以控制单个Redis实例的最大内存,以尽量减少Redis在fork时的耗时;或控制自动触发的频率以减少分叉次数。AOF的原理AOF的整个过程大致可以分为两步。第一步是实时写命令(如果配置了appendfsynceverysec,会有1s的丢失),第二步是重写aof文件。增量追加到文件的主要过程是:(1)写入命令(2)追加到aof_buf(3)同步到aof磁盘,那为什么要先写buf再同步到磁盘呢?如果实时写入磁盘会带来非常高的磁盘IO,影响整体性能。AOFrewrite你可能会想每一个写命令都会产生一个日志,那么AOF文件会不会很大呢?答案是肯定的,AOF文件会越来越大,所以Redis提供了另外一个功能,叫做AOFrewrite。它的作用是重新生成一个AOF文件。新AOF文件中一条记录的操作只会执行一次,不像旧文件可能记录对同一个值的多次操作。手动触发:根据配置规则触发bgrewriteaof的自动触发。当然,整体自动触发的时间也和Redis的定时任务频率有关。我们看一下改写后的流程图:(1)redis调用fork,现在有父子两个进程(2)子进程根据数据库快照,将重建数据库状态的命令写入临时文件在内存中(3)父进程继续处理客户端请求,除了向原aof文件写入写命令。同时缓存接收到的写命令。这样可以保证如果子进程rewrite失败,不会有问题。(4)子进程将快照内容写入临时文件后,子进程发送信号通知父进程。然后父进程也将缓存的写命令写入临时文件。(5)现在父进程可以使用临时文件替换旧的aof文件并重命名,后面收到的write命令也会开始追加到新的aof文件中。PS:需要注意的是,重写aof文件的操作并不是读取旧的aof文件,而是通过命令将整个内存中的数据库内容重写为新的aof文件,有点类似于快照。为什么Redis这么快?Redis使用基于内存的单进程单线程模型的KV数据库,用C语言编写,官方数据可以达到100,000+QPS(每秒查询次数)。这个数据不比Memcached差,同样是采用单进程多线程的基于内存的KV数据库!原因如下:1、完全基于内存,大部分请求都是纯内存操作,速度非常快;2.数据结构简单,数据操作也容易简单,Redis中的数据结构是专门设计的;3.使用单线程避免了不必要的上下文切换和竞争条件,也没有多进程或多线程导致的切换导致的CPU消耗,所以不需要考虑各种锁的问题,没有操作加锁和释放锁,不存在死锁造成性能消耗的可能;4、采用多路I/O复用模型,非阻塞IO;5、使用的底层模型不同,底层的实现方法和与客户端通信的应用协议不同。Redis直接构建了自己的VM机制。多通道I/O多路复用模型多通道I/O多路复用模型是使用select、poll、epoll同时监听多个流的I/O事件的能力。空闲时,当前线程会被阻塞。当一个或多个流有I/O事件时,它们会从阻塞状态中醒来,所以程序会轮询所有的流(epoll只轮询那些真正发出事件的流),并且只顺序执行这种做法避免了很多无用的通过处理就绪流的操作。这里的“多路复用”是指多个网络连接,“多路复用”是指多路复用同一个线程。使用多I/O多路复用技术,可以让单个线程高效处理多个连接请求(最小化网络IO的时间消耗)。Redis中的事务是命令的集合。和命令一样,事务是Redis的最小执行单元。事务中的所有命令都执行或不执行。Redis事务的实现需要两个命令,MULTI和EXEC。当事务开始时,先向Redis服务器发送MULTI命令,然后依次发送本次事务中需要处理的命令,最后发送EXEC命令表示事务命令结束。例如使用redis-cli连接redis,然后在命令行工具中输入如下命令:从输出结果可以看出,输入MULTI命令后,服务端返回OK表示事务启动成功,然后输入中执行的所有命令,每输入一个命令,服务器不会立即执行,而是返回“QUEUED”,表示该命令已被服务器接受并暂时保存。***输入EXEC命令后,这个事务中的所有命令都会依次执行。可以看到***服务器一次返回了三个OK。这里返回的结果和发送的命令是有序的,也就是说本次交易中的所有命令都执行成功了。再比如,在命令行工具中输入如下命令:和前面的例子一样,先输入MULTI***,再输入EXEC,表示中间的命令属于一个事务。不同的是,中间输入的命令有错误(set写成sett),这样因为有错误的命令,交易中的其他命令都没有执行,可以看出交易中的所有命令是一致的。如果客户端在发送EXEC命令之前断开连接,服务器会清空事务队列,事务中的所有命令都不会被执行。一旦客户端发送了EXEC命令,事务中的所有命令都会被执行,即使之后客户端断开连接也没关系,因为服务端已经保存了事务中的所有命令。Redis事务除了保证一个事务中的所有命令要么全部执行完,要么根本不执行,还可以保证一个事务中的命令顺序执行,不被其他命令插入。想象一下,客户端A需要执行几条命令,客户端B同时发送几条命令。如果不使用事务,客户端B的命令可能会插入到客户端A的几条命令中,如果要避免这种情况发生,也可以使用事务。Redis事务错误处理如果事务中出现了命令执行错误,Redis会如何处理呢?要回答这个问题,首先要弄清楚是什么原因导致了命令执行错误:1.语法错误:就像上面的例子,语法错误表示命令不存在或者参数错误。这种情况下,就需要区分Redis的版本了。Redis2.6.5之前的版本会忽略错误的命令,执行其他正确的命令。2.6.5之后的版本将忽略事务。不执行所有命令。2、运行错误运行错误是指在命令执行过程中出现错误,比如使用GET命令获取哈希表类型的键值。这种错误在命令执行之前是无法被Redis发现的,所以这样的命令会在事务中被Redis接受并执行。如果食物中的其中一条命令执行错误,其他命令仍然会被执行(包括错误之后的命令)。Redis中的事务没有关系数据库中的事务回滚(rollback)功能,所以用户必须自己清理剩下的烂摊子。但是,由于Redis不支持事务回滚功能,这也使得Redis事务变得简单快速。WATCH、UNWATCH、DISCARD命令从上面的例子我们可以看出,每个命令的结果只有在事务中的所有命令都执行完之后才能得到,但是如果一个事务中的命令B依赖于他上一个命令的结果我该怎么办?比如在Java中实现类似i++的功能,必须先获取当前值,然后才能对当前值加一。这种情况下,不可能只使用上面介绍的MULTI和EXEC,因为MULTI和EXEC中的命令是一起执行的,其中一个命令的执行结果不能作为另一个命令的执行参数,所以这时有必要介绍一下Redis事务家族的另一个成员:WATCH命令。换个角度思考上面提到的实现i++的方法,可以这样实现:监控i的值,确保i的值没有被修改获取i的原始值如果i的值没有被修改在此过程中,i+1的当前值。否则,可以在不执行WATCH命令的情况下监视一个或多个键。一旦其中一个键被修改(或删除),后续交易将不会执行。监听一直持续到EXEC命令(事务中的命令在EXEC命令执行后,被监听的key会自动UNWATCH)。例如:在上面的例子中,先将mykey的键值设置为1,然后使用WATCH命令监控mykey,然后将mykey的值改为2,然后进入交易,在transaction,然后执行EXEC运行对于事务中的命令,***使用get命令查看mykey的值,发现mykey的值还是2,说明事务中的命令不是根本不执行(因为在WATCH监控mykey的时候修改了mykey,所以后面的事务会被取消)。UNWATCH命令可以在WATCH命令执行后,MULTI命令执行前取消对某个键的监听。例如:在上面的例子中,先将mykey的key值设置为1,然后使用WATCH命令监控mykey,然后将mykey的值改为2,然后取消对mykey的监控,然后进入事务,设置事务中mykey的值为3,然后执行EXEC运行事务中的命令,***使用get命令查看mykey的值,发现mykey的值还是3,说明事务中的命令成功运行。DISCARD命令可以在MULTI命令执行后、EXEC命令执行前取消WATCH命令并清空事务队列,然后退出事务状态。例如:在上面的例子中,先将mykey的键值设置为1,然后使用WATCH命令监控mykey,然后将mykey的值改为2,然后进入交易,在事务,然后执行DISCARD命令,然后在EXEC运行事务中执行命令,发现报错“ERREXECwithoutMULTI”,说明DISCARD命令执行成功——取消WATCH命令,清空事务队列,然后退出事务状态。上面介绍的Redis分布式锁中的RedisWATCH、MULTI、EXEC命令,如果数据被其他客户端抢先修改,只会通知执行这些命令的客户端,从而取消对数据的修改,并不能阻止其他客户端修改了数据,所以只能称为乐观锁。而且这种乐观锁是不可扩展的——当客户端试图完成一个事务时,可能会因为事务执行失败而反复重试。保证数据的准确性很重要,但是当负载变大的时候,使用乐观锁就不安全了。这时候就需要使用Redis来实现分布式锁。分布式锁:是一种控制分布式系统之间共享资源同步访问的方式。在分布式系统中,经常需要协调它们的动作。如果不同系统或同一系统的不同主机共享一个或一组资源,在访问这些资源时往往需要互斥,以防止相互干扰,保证一致性。在这种情况下,你需要使用分布式锁。Redis命令介绍:Redis用来实现分布式锁的主要命令是SETNX命令(SETifNoteXists)。语法:SETNXkeyvalue功能:当且仅当key不存在时,将key的值设置为value并返回1;如果给定的key已经存在,SETNX什么都不做,返回0。使用Redis建锁:思路:设置“lock:”+参数名作为锁的key,使用SETNX命令尝试设置一个随机uuid作为锁的值,并设置锁的过期时间,使用SETNX设置锁的值。防止其他进程获取锁。如果尝试获取锁失败,程序将不断重试,直到成功获取锁或超过给定的时间限制。代码:publicStringacquireLockWithTimeout(Jedisconn,StringlockName,longacquireTimeout,longlockTimeout){Stringidentifier=UUID.randomUUID().toString();//锁定值StringlockKey="lock:"+lockName;//锁定密钥intlockExpire=(int)(lockTimeout/1000);//锁的过期时间longend=System.currentTimeMillis()+acquireTimeout;//尝试获取锁的时限while(System.currentTimeMillis()
