一、事务的认识
redis事务和mysql的事务相似,但是比它的简单。
原子性、一致性、持久性、隔离性。
Redis的事务和MySQL一比就是一个弟弟。
原子性:Redis的事务到底有没有原子性,存在争议。最原本的含义,是吧多个操作打包到一起,要么全都执行,要么全都不执行。Redis做到了上述的含义。但是MySQL这里的原子性,走的更远,也是把多个操作打包到一起,呀么全都执行成功,但是Redis不保证成功。如果事务中若干个操作,存在有失败的,就不管了,而MySQL进行回滚。把中间已经执行的操作,全都回退了。
新版本。
不具备一致性:redis没有约束,也没有回滚机制,事务执行过程中如果某个修改操作出现失败,就可能引起不一致的情况。
不具备持久性:Redis本身就是内存数据库,数据是存储在内存中,虽然Redis也有持久化机制,但是这里的持久化机制,和事务没有啥直接关系。
不涉及隔离性:Redis是一个单线程模型的服务器程序,所有的请求/事务,都是串行执行的。
Redis的事务意义
Redis的事务,主要的意义,就是为了打包,避免其他客户端的命令,插队插到中间。Redis中实现事务,是引入了队列每个客户端都有一个,开启事务的时候,此时客户端输入的命令,就会发给服务器并且进入这个队列中,而不是立即执行。当遇到执行事务命令的时候,此时就会把队列中的这些任务都按照顺序一次执行。Redis主线程完成的。主线程会把事务中的操作都执行完,再处理别的客户端。
Redis的事务为啥搞得这么简单,为啥不设计成和MySQL一样强大呢?
MySQL的事务,在背后付出了很大的代价,空间上要花费更多的空间来存储更多的数据。时间上,也要有更大的执行开销。正是因为MySQL上述的问题,才有了Redis上场的机会。
Redis事务的应用场景?
如果我们需要把多个操作打包进行,使用事务时比较合适的。秒杀,ROG掌机称为持家之眼,最大的问题就是买不到,每隔一段时间放一批货,你得去某东上抢。黄牛问题,不到1s就没了,不能写个程序,自动抢。不会因为黄牛就没用程序抢吧?超卖:放货5000台,实际如果让5001个人下单成功,就是超买了。一个典型的写法,获取仓库中剩余的商品个数,如果个数 》 0 下单成功,商品个数减一,如果不加上任何限制,就可能会存在线程安全的问题。以前在多线程中,是通过加锁的方式,来避免插队的。在Redis中就直接使用事务即可!
第二个客户端的执行事务,命令发过来之后,服务器才真正执行第二个事务中的内容,此时第一个事务执行事务命令已经运行过了。此时第二个事务get到的count就已经是第一个事务自减之后的结果了,这个场景中,没加锁,也能解决超卖问题。其实这是保证了执行操作的执行顺序。确实Redis的事务的应用场景,没有MySQL的事务那么多。Redis命令里能进行条件判断吗?redis原生命令中确实没有这种判断,但是redis支持lua脚本。通过lua脚本就可以实现上述的条件判定,并且也和事务一样是打包批量执行的。lua脚本的实现方式是redis事务的进阶版本。
事务的相关命令
开启事务 MULTI
在服务器的事务队列中,保存了上述的请求,此时如果另外开一台客户端,再尝试查询这几个key对应的数据,是查询不到的。是没有结果的
执行事务 EXEC
放弃当前事务 DISCARD
当我们开启事务,并且,给服务器发送若干个命令之后,此时服务器重启,此时的效果其实就等同于DISCARD。
WATCH监控某个key是否在事务执行之前发生了改变
从时间上来看,客户端1是先发送了set key 222,客户端2是后发送了set key 333
由于客户端1中,得是exec执行了,才会真正执行set key 222,这个操作变成了实际上更晚执行的操作,最终值就是222。在刚才的场景中,就可以使用watch命令来监控这个key,看看这个key在事务multi和exec之间,set key之后是否在外部被其他客户端修改了。
此时,exec在执行上述事务中的命令的时候,发现key在外部有修改。于是真正执行set key 111的时候,就没有真正的执行。NWATCH可以取消WATCH的操作。
WATCH的实现原理:
watch的实现,类似于一个乐观锁。乐观锁/悲观锁不是指具体的锁,而是指的是某一类锁的特性。乐观锁:加锁之前,就有一个心理预期,预期接下来所冲突的概率比较低。
悲观锁:加锁之前,也有一个心里预期,接下来锁冲突的概率比较高。
锁冲突概率高和冲突比较低,接下来要做的工作是不一样的。
redis的watch就相当于是基于版本号这样的机制,来实现了乐观锁。
当执行watch key的时候,就会给这个key安排一个版本号,版本号可以理解成一个整数。每次在修改的时候,版本号都会变大。注意!watch必须搭配事务使用,并且必须在multi之前使用。在执行事务中命令的时候,会做出判定,判定当前这个key的版本号和最初watch的时候,记录的版本号是否一致,如果一致,说明当前key的事务开启到最终执行这个过程中,没有被别的客户端修改,于是才能真正进行设置,如果不一致,说明key在其他客户端中改过了,因此此处就直接丢弃事务中的操作。exec返回nil。
事务小结:Redis的事务,要比MySQL的事务,简单很多。
1、原子性:Redis的事务,并不支持回滚。
2、一致性:Redis并不会保证事务执行前和执行后内容统一。
3、持久性:Redis主要通过内存来存储数据。
4、隔离性:Redis自身作为一个单线程的服务器模型,上面处理请求本质上都是串行执行的。
multi、exec、discard、watch/unwatch。Redis中的lua脚本,也能起到类似于事务的效果。官方网站上说,事务这里的任何实现的效果,都可以使用lua脚本代替。
二、主从复制
分布式系统中,涉及到一个非常关键的问题:单点问题。
如果某个服务器程序,只有一个节点,只有一个物理服务器,来部署这个服务器程序。
1、可用性问题,如果这个机器挂了、服务就中断了。
2、性能/支持的并发量也是比较有限的。
引入分布式系统,主要也就是为了解决上述的单点问题。在分布式系统中,往往希望有多个服务器来部署redis服务,从而构成一个redis集群,此时就可以让这个集群给整个分布式系统中其他的服务,提供更稳定/更高效的数据存储功能。
在分布式系统中,希望使用多个服务器来部署redis,存在以下几种redis的部署方式:
1、主从模式
2、主从 + 哨兵模式
3、集群模式
接下来我们介绍主从模式
在若干个redis节点中,有的是主节点,有的是从节点。假设有三个物理服务器。分别部署了一个redis-server进程。此时就可以把其中的一个节点,作为主节点。另外两个节点作为从节点,从节点得听主节点的。从节点上的数据要跟随主节点变化,从节点的数据要和主节点保持一致。本来,在主节点上保存一堆数据,引入从节点之后,就是要把主节点上面的数据复制出来,放到从节点中,后续,主节点这边对于数据有任何修改,都会把这样的修改给同步到从节点上。从节点就是主节点的一个副本,如果我改了从节点的数据,是否是把从节点的数据往主节点上同步呢?在Redis主从模式中,从节点上的数据,不允许修改!只能读取数据。
这样做的好处有什么呢?
性能:由于从节点的数据都是时刻和主节点保持一致的。因此其他的客户端从从节点这里读取数据,和从主节点读取数据是没有区别的。后续如果有客户端来读取数据了,就可以从上述节点中,随机挑一个,给这个客户端提供读取数据的服务。引入了更多的计算资源,自然能够支撑的并发量也就大幅提高了。
可用性问题:之前只是单个redis服务器节点,此时这个机器挂了,整个redis就挂了。上述这个主从结构中,这些redis的机器不太可能同时挂了。但是整个机房是否可能被一锅端了?也是可能会存在的。这个时候如果考虑到更高的可用性,就可以把这些机器放到多个不同的机房中。
如果挂掉了某个从节点,没啥影响。此时继续从主节点或者其他从节点读取数据,得到的效果完全相同!如果是挂掉了主节点呢?还是有一定影响的。从节点只能读数据,如果需要写数据,就没得写了。可用性提高了,但是还没到非常理想的程度。如果存在两个主节点,相互之间如何同步数据?会是个麻烦事。
更准确的说,主从模式主要是针对读操作,进行并发量&可用性提高。而写操作的话,无论是可用性还是并发,都是非常依赖主节点,主节点又不能高多个。实际业务场景中,读操作往往就是比写操作更频繁。主从结构,是分布式系统中,比较经典的一种结构,不仅仅是redis支持,像MySQL等其他的常用组件也是支持的。
配置主从节点
配置redis主从结构,首先需要启动多个redis服务器。正常来说,每个redis服务器程序,应该在一个单独的主机上才是分布式。但是我们目前只有一个云服务器。可以在一个云服务器主机上,运行多个redis-server进程的。此处需要保证多个redis-server的端口是要不同的。本来默认的端口是6379,此时就不能让新启动的redis-server再继续使用6379端口号了。
如何去指定redis-server的端口,
1、可以启动程序的时候,通过命令行来指定端口号 --port选项。
2、也可以直接再配置文件中,来设定端口。
此处准备搞一个主节点,两个从节点,主节点的端口号不变。修改端口号。
这个配置进行修改,按照后台进程的方式进行运行。
当前这几个节点,并没有构成主从结构,而是各自为政。要想成为主从结构,还需要进一步的配置。
配置主从复制:
要想配置成主从结构,就需要使用slaveof。
以上三种方法中,使用更多的是通过配置文件来配置主从服务,修改了配置文件,一直持久生效的。
6379为主节点,6382和6381为从节点。
redis服务器的配置文件改完了之后,需要重新启动这个服务器。
通过kill -9命令停止,这种停止redis-server的方式,是和咱们之前通过直接运行redis-server命令的方式搭配的。而如果是使用service redis-server start 这种方式启动的,就必须使用service redis-server stop来进行停止。此时如果使用kill -9命令的方式停止,kill掉之后,这个redis-server进程能自动启动。服务器就是要稳定性和高可用。但是服务器上的某些程序,可能也难以避免出现挂了的情况,如果服务器进程挂了,要是能自动启动进程,对于整体的服务不会产生严重影响。往往会有另外一个进程来专门监控指定的服务器进程的运行状态。
查看主从节点的结构
执行aplication命令
主节点上收到源源不断的修改数据请求,从节点就需要从主节点这里同步这些修改请求,从节点和主节点之间的数据同步,不是瞬间完成的!offset就相当于是从节点和主节点之间,同步数据的进度。
lag延迟。
积压缓冲区,支持部分同步机制的实现。
断开主从复制的关系。
slaveof no one 直接在redis客户端命令窗口书写。
从节点断开主从关系,它就不再从属于其他节点了,里面已经有的数据,是不会抛弃的!但是,后续节点如果针对数据做出修改,从节点就无法再自动同步数据了。
让这个从节点,直接跑路到其他节点作为其他节点的从节点
主从复制时的安全
主从复制只是从主到从。不能从 从节点到主节点。如果从节点允许修改,后续对于从节点的修改,主节点是感知不到的,数据就不一致了。
主节点和从节点之间通过网络来传输的。TCP内部支持了nagle算法。开启了就会增加tcp的传输延迟,节省了网络带宽。关闭了,就会减少tcp的传输延迟,增加了网络带宽。
这个选项可以用于在主从同步通信过程中,关闭tcp的nagle算法。从节点更快速的和主节点进行同步。一般游戏开发,尤其是即时性要求很高的。
主从复制的拓扑结构
若干个节点之间,按照啥样的方式来进行组织连接
一主一从
如果写数据请求太多,此时也是会给主节点造成一些压力,可以通过关闭主节点的AOF,只在从节点上开启AOF。但是这种设定方式,有一个严重的缺陷,主节点一旦挂了,不能让他自动重启。如果自动重启,此时没有AOF文件,就会丢失数据,进一步的主从同步,会把从节点的数据也给删了。改进办法,当主节点挂了之后,就需要让主节点从 从节点这里获取到AOF的文件。
实际开发中,读请求,远远多于写请求。
主节点上的数据发生改变,就会把改变的数据同时同步给所有的从节点。随着从节点个数的增加,同步一条数据,就需要传输多次。
树形主从结构
主节点就不需要那么高的网卡带宽了。一旦数据进行修改了,同步的延时是比刚才更长的。
主从复制的基本流程
redis提供了psync命令,完成数据同步的过程。psync不要咱们手动执行,redis服务器会在建立好主从同步关系之后,自动执行psync。从节点负责执行psync,从节点从主节点这边拉取数据。
replicationid的作用:主节点生成的复制id,主节点启动的时候就会生成。即使是同一个主节点,每次重启生成的id都是不同的。从节点和主节点建立了复制关系,就会从主节点这边获取到replicatio-n id。info replication 获取到当前replication id 的值。
replid2这个是用不上的。有一个主节点A,还有一个从节点B。A会生成replid,B获取到replid。如果A和B通信过程中出现了一些网络抖动。B可能就会认为A挂了。B就会自己成为主节点。给自己生成一个replid。此时,B也会记得之前旧的replid,就是通过replid2。后续网络稳定了,B还可以根据replid2重新回到A的怀抱。需要手动干预,哨兵机制,可以自动完成这个过程。
offset的作用:偏移量,主节点和从节点上都会维护这个偏移量。主节点的偏移量,主机点上会收到很多的修改操作的命令,每个命令都要占据几个字节。主节点会把这些修改命令,每个命令的字节数进行累加。从节点的偏移量,就描述了现在从节点这里数据同步到哪里了。如果从节点这边的偏移量和主节点的偏移量一样了。说明主从数据相同。从节点每秒钟上报自身的复制偏移量。replication id和offset共同描述了一个数据集合。如果发现两个机器replication id一样,offset也一样,就可以认为这两个redis机器上存储的数据就是完全一样的!
psync运行流程
从节点发送psync命令给主节点,replid和offset的默认值分别是?和 -1。
主节点根据psync参数和自身数据情况决定响应结果:
psync这里可以从主节点获取全量数据,也可以获取一部分数据。
主要就是看offset这里的进度。offset 写作-1就是获取全量数据。offset写具体的正整数,则是从当前偏移量来进行获取。
获取所有数据,是最稳妥的,但是会比较低效,如果从节点之前已经从主节点这里复制过一部分数据了,就只需要把新的之前没复制过的数据搞过来即可。不是从节点索要哪部分数据,主节点就一定给哪部分,主节点会自行判定,看当前是否方便给部分数据,不方便只能给全量了。
啥时候进行全量复制:
1、首次和主节点进行数据同步。
2、从节点不方便进行部分复制的时候。
啥时候进行部分复制:
从节点之前已经从主节点上复制过数据了。因为网络抖动或者从节点重启了。从节点需要重新从主节点这边同步数据此时看看能不能只同步一小部分,大部分数据都是一致的。
全量同步:
全量复制的无硬盘模式:
主节点,进行全量复制的时候,也支持无硬盘模式。主节点生成的rdb的二进制数据,不是直接保存到文件中了,而是直接进行网络传输了。从节点之前也是先把收到rdb数据,写入到硬盘中,然后再加载硬盘,现在可以省略这个问题,直接把收到的数据进行加载了。即使引入了无硬盘模式,仍然整个操作时比较重量的,比较耗时的,网络传输时没法省略的。相比于网络传输来说,读写硬盘是小头。
runid和replid
一个redis服务器上replid和runid都是存在的,但是是两个不同的id,看起来非常像。
长度相同,格式也非常相似。
run id是每个节点都不相同的,replid 则是具有主从关系的节点,是相同的。
run_id标识一次redis_server的运行。
上图是redis源码中使用到run_id的主要文件,redis哨兵,runid主要是用在支撑实现redis哨兵这个功能的。和主从复制没有任何关系
而replid是出现在replication.c这个源文件中的。 这个代码是进行主从复制的逻辑!replid主要是起到主从复制的作用。
当客户端输入psync命令就会去执行这个命令。
replid + offset共同标识了一个数据集合。
增量同步(部分复制)执行流程
从节点要从主节点这里进行全量复制,全量复制,开销是最大的。有些时候,从节点本身已经持有了主节点的绝大部分数据,这个时候,就不太需要进行全量复制了。比如出现网络抖动,主节点这边最近修改的数据就无法即时同步过来了,更严重的是,从节点已经感知不到主节点了。进一步的从节点可能会升级成主节点。网络抖动,一般都是暂时的。过一会就恢复了。此时就可以让从节点和主节点重新建立联系了。当从节点和主节点重新建立连接之后,就需要进行数据的同步。一样会发起psync命令带有具体的relid和offset值,主机点就要根据psync的参数进行判定,当前这次是按照全量复制合适还是部分复制合适。
积压缓冲区的属性,就是一个内存中的简单队列。
如果超过了积压缓冲区的大小,那就只能全量复制。
全量复制:从节点刚连上主机点之后,进行的数据初始化工作。
部分复制:全量复制的特殊情况,优化手段,目的和全量复制一样。
实时复制的流程:从节点已经和主节点同步好了数据了,从节点这一时刻已经和主节点数据一致了。但是之后,主节点这边会源源不断的收到新的修改数据的请求。住节点上的数据就会随之改变。也需要能够同步给从节点,从节点和主节点之间会建立TCP长连接。然后主节点收到的修改数据的请求,通过上述连接,发给从节点,从节点再更具这些修改请求,修改内存中的数据。这个过程也是需要时间的,但是时间很短。正常来说,这个延时是比较短的,但是如果是多级从节点,树形结构的拓扑。如果这里的从节点有很多层,延时也就会上升。
在进行实时复制的时候,需要保证连接处于可用状态,心跳包机制:
主节点:默认每隔10s给从节点发送一个ping命令,从节点收到就返回pong。从节点:默认每隔1s就给主节点发起一个特定的请求,就会上报当前从节点复制数据的进度offset。
回顾:
主要解决问题:
1、单个redis节点,可用性不高。
2、单个redis节点,性能有限。
主从复制的特点
主从复制配置过程
主从复制的缺点
主从复制最大的问题还是再主节点上,主节点挂了,从节点就迷茫了,虽然能够提供读操作,但是从节点不能自动的升级成主节点。不能替换原有主节点对应的角色。此时,就需要程序员/运维手工的恢复主节点。这样的过程是非常繁琐的。为了解决这个问题。我们引入了Redis哨兵机制。自动的对挂了的主节点进行替换。