前言
先看一下计算机硬盘的缓存设计。硬盘的缓存主要起三种作用:
- 预读取
当硬盘受到CPU指令控制开始读取数据时,硬盘上的控制芯片会控制磁头把正在读取的簇的下一个或者几个簇中的数据读到缓存中(由于硬盘上数据存储时是比较连续的,所以读取命中率较高),当需要读取下一个或者几个簇中的数据的时候,硬盘则不需要再次读取数据,直接把缓存中的数据传输到内存中就可以了,由于缓存的速率远远高于磁头读写的速率,所以能够达到明显改善性能的目的。
- 写入
当硬盘接到写入数据的指令之后,并不会马上将数据写入到盘片上,而是先暂时存储在缓存里,然后发送一个“数据已写入”的信号给系统,这时系统就会认为数据已经写入,并继续执行下面的工作,而硬盘则在空闲(不进行读取或写入的时候)时再将缓存中的数据写入到盘片上。虽然对于写入数据的性能有一定提升,但也不可避免地带来了安全隐患——数据还在缓存里的时候突然掉电,那么这些数据就会丢失。对于这个问题,硬盘厂商们自然也有解决办法:掉电时,磁头会借助惯性将缓存中的数据写入零磁道以外的暂存区域,等到下次启动时再将这些数据写入目的地。
- 临时存储
临时存储
有时候,某些数据是会经常需要访问的,像硬盘内部的缓存(暂存器的一种)会将读取比较频繁的一些数据存储在缓存中,再次读取时就可以直接从缓存中直接传输。缓存就像是一台计算机的内存一样,在硬盘读写数据时,负责数据的存储、寄放等功能。这样一来,不仅可以大大减少数据读写的时间以提高硬盘的使用效率。同时利用缓存还可以让硬盘减少频繁的读写,让硬盘更加安静,更加省电。更大的硬盘缓存,你将读取游戏时更快,拷贝文件时候更快,在系统启动中更为领先。
其实在做软件设计的时候,要想在有限的资源情况下提高系统的效率,也可以参照计算机的设计思路做缓存。如果把数据库中的数据比作是硬盘,那么我们的系统就需要类似硬盘缓存的一种方案。最好的就是将数据库存储引擎的数据加载到内存中。目前内存数据库还处于发展阶段,相信未来某天我们的数据不再依赖硬盘,或者说硬盘的读写速度发生了变革性的提升,不过在这一天到来之前,作为一个小程序员,还是好好学习学习缓存技术。
搭建Redis服务
这里使用yum的方式安装redis。yum install -y redis.安装的redis版本为3.2.12
[root@daice ~]# yum list installed | grep redis
redis.x86_64 3.2.12-2.el7 @epel
修改/etc/redis.conf配置文件
bind 127.0.0.1 #ip
port 6379 #端口
protected-mode no #设置为no就可以使其他的hosts来连接本机的redis
daemonize yes # 设置以守护进程的方式运行redis
pidfile /var/run/redis_6379.pid #如果设置了daemonize yes,则会生成这个pidfile.注意如果单机多个redis不要让这个file有重名
logfile /var/log/redis/redis.log #redis的日志文件
requirepass 123456 #设置连接redis的密码
直接使用systemctl start redis 启动redis。启动后查看redis服务的状态
[root@daice ~]# systemctl status redis
● redis.service - Redis persistent key-value database
Loaded: loaded (/usr/lib/systemd/system/redis.service; enabled; vendor preset: disabled)
Drop-In: /etc/systemd/system/redis.service.d
└─limit.conf
Active: active (running) since Fri 2020-08-28 16:36:49 CST; 12min ago
Process: 29233 ExecStop=/usr/libexec/redis-shutdown (code=exited, status=1/FAILURE)
Main PID: 29328 (redis-server)
Tasks: 3
Memory: 5.0M
CGroup: /system.slice/redis.service
└─29328 /usr/bin/redis-server *:6379
将redis服务加入到开机启动
systemctl enable redis
连接redis
redis-cli -h 127.0.0.1 -p 6379 -a 123456
主从模式
在实际项目中一般不会使用单机版的redis.先从最基本的主从模式入手。主从模式有以下的特点
- 一般情况下主节点可读可写,从节点只读。
- slave自动同步master的数据
- 一个master可以有多个slave,但一个slave只能对应一个master
- slave挂了不影响其他slave的读和master的读和写,重新启动后会将数据从master同步过来
- master挂了以后,不影响slave的读,但redis不再提供写服务,我们需要手动的重启master或者重新设置master
主从模式的配置
这里是基于yum安装的方式。资源有限,就在同一台服务器上做了。复制一份/etc/redis.conf。 cp /etc/redis.conf /etc/redis_slave01.conf.这里需要注意一下这个文件的所属用户。如果我们使用的root,那么需要执行一下
chown redis:root redis_slave01.conf
具体的用户和用户组根据实际情况来更改,保证和最初的redis.conf一致
[root@daice ~]# ll /etc/redis*
-rw-r----- 1 redis root 46725 Aug 26 14:38 /etc/redis.conf
-rw-r----- 1 redis root 7642 Oct 26 2018 /etc/redis-sentinel.conf
-rw-r----- 1 redis root 46706 Aug 28 16:35 /etc/redis_slave01.conf
修改redis_slave01.conf
port 6479 #端口设置为6479
pidfile /var/run/redis_6479.pid #修改pidfile
logfile /var/log/redis/redis_slave01.log #修改log文件
slaveof 127.0.0.1 6379 #设置master的ip port ,可以搜索# Master-Slave replication.找到master/slave的配置说明
masterauth 123456 #设置master的认证密码
requirepass 123456 #设置连接该节点的认证密码
修改好后,我们增加一个service.
使用yum安装的好处就是,已经有写好的redis.service。我们复制一份 cp redis.service redis_slave01.service
修改redis_slave01.service,指定好配置文件
ExecStart=/usr/bin/redis-server /etc/redis_slave01.conf --supervised systemd
接下来我们先启动master,在启动slaver
systemctl restart redis
systemctl start redis_slave01
测试一下master:
[root@daice system]# redis
127.0.0.1:6379> set name jack
OK
127.0.0.1:6379> get name
"jack"
测试一下slave:
[root@daice system]# redis-cli -h 127.0.0.1 -p 6479 -a 123456
127.0.0.1:6479> keys *
1) "name"
127.0.0.1:6479> set city beijing
(error) READONLY You can't write against a read only slave.
127.0.0.1:6479> get name
"jack"
我们看到slave可以查询到master设置的name,并且是只读模式。可以使用info replication查看集群的信息:
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:1
slave0:ip=127.0.0.1,port=6479,state=online,offset=5509,lag=1
master_repl_offset:5509
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:2
repl_backlog_histlen:5508
哨兵模式
因为主从模式中主节点挂掉后就无法提供写服务,我们就可以使用redis的哨兵模式,如果master挂掉,可以自动的从slave中选出一个来当master。哨兵模式的特点如下:
- 哨兵是一个单独的进程来监控redis集群
- 哨兵模式是基于主从模式的,如果master挂掉,会从slave中选举一个修改他的配置作为master,同时其他的slave也会修改slaveof 指向新的master
- 当master重启后,它将作为slave而不再是master
哨兵模式的实现
哨兵是一个单独的进程,用于监控redis的主从节点。既然哨兵也是一个进程,也有宕掉的风险,所以实际使用中,也会启用多个哨兵。当一个哨兵向master发送ping命令,如果master在配置的down-after-milliseconds时间内没有返回响应,那么就会将该master标记为主观下线。当有一定数量的哨兵都做了标记,这时候会发起投票选举出一个slave作为master并通过发布订阅模式通知到其他的slave更改配置文件,切换master主机,这个时候原来的master则是客观下线。
哨兵模式的配置
按照上面的主从配置在增加一个slave:
[root@daice system]# redis
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:2
slave0:ip=127.0.0.1,port=6479,state=online,offset=10227,lag=1
slave1:ip=127.0.0.1,port=6579,state=online,offset=10227,lag=1
master_repl_offset:10227
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:2
repl_backlog_histlen:10226
配置三个哨兵:
在/etc 下有redis-sentinel.conf的配置文件,编辑这份文件:
protected-mode no
port 26379
sentinel monitor mymaster 127.0.0.1 6379 2 #mymaster可以自定义名称,后面是master的ip及端口。2 表示的是有多少个sentinel主观下线后会开始更换新的master
sentinel auth-pass mymaster 963300 #认证信息
logfile "/var/log/redis/sentinel.log" #日志
复制两份(注意使用的用户).redis-sentinel-01.conf和redis-sentinel-02.conf。分别更改这两份文件的端口及日志的配置即可,我这边分别配置成了26389和26399.
切到/lib/systemd/system目录,将redis.service复制两份,redis-sentinel-01.service和redis-sentinel-02.service,分别编辑复制的两份文件,将ExecStart关联的配置文件配置为对应的conf文件即可
ExecStart=/usr/bin/redis-sentinel /etc/redis-sentinel-01.conf --supervised systemd
ExecStart=/usr/bin/redis-sentinel /etc/redis-sentinel-02.conf --supervised systemd
systemctl分别启动这三个服务即可。
启动后查看日志:
_.-``__ ''-._
_.-`` `. `_. ''-._ Redis 3.2.12 (00000000/0) 64 bit
.-`` .-```. ```\/ _.,_ ''-._
( ' , .-` | `, ) Running in sentinel mode
|`-._`-...-` __...-.``-._|'` _.-'| Port: 26389
| `-._ `._ / _.-' | PID: 2500
`-._ `-._ `-./ _.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' | http://redis.io
`-._ `-._`-.__.-'_.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' |
`-._ `-._`-.__.-'_.-' _.-'
`-._ `-.__.-' _.-'
`-._ _.-'
`-.__.-'
2500:X 31 Aug 14:14:18.064 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
2500:X 31 Aug 14:14:18.067 # Sentinel ID is fa2091eda9d965657db8f7394bc772ac39e07552
2500:X 31 Aug 14:14:18.067 # +monitor master mymaster 127.0.0.1 6379 quorum 2
2500:X 31 Aug 14:14:18.067 * +slave slave 127.0.0.1:6479 127.0.0.1 6479 @ mymaster 127.0.0.1 6379
2500:X 31 Aug 14:14:18.070 * +slave slave 127.0.0.1:6579 127.0.0.1 6579 @ mymaster 127.0.0.1 6379
2500:X 31 Aug 14:14:19.950 * +sentinel sentinel 42f525c6a03ea6154e87b92d0d52c4e88a48f67e 127.0.0.1 26379 @ mymaster 127.0.0.1 6379
2500:X 31 Aug 14:27:05.422 * +sentinel sentinel fb3ff1953636ae005e4bd529ff52c4408d7b4dab 127.0.0.1 26399 @ mymaster 127.0.0.1 6379
测试一下,systemctl stop redis关闭master节点。查看sentinel的日志:
2500:X 31 Aug 14:48:18.928 # +sdown master mymaster 127.0.0.1 6379
2500:X 31 Aug 14:48:18.987 # +odown master mymaster 127.0.0.1 6379 #quorum 3/2
2500:X 31 Aug 14:48:18.987 # +new-epoch 1
2500:X 31 Aug 14:48:18.987 # +try-failover master mymaster 127.0.0.1 6379
2500:X 31 Aug 14:48:18.990 # +vote-for-leader fa2091eda9d965657db8f7394bc772ac39e07552 1
2500:X 31 Aug 14:48:18.995 # 42f525c6a03ea6154e87b92d0d52c4e88a48f67e voted for fa2091eda9d965657db8f7394bc772ac39e07552 1
2500:X 31 Aug 14:48:18.995 # fb3ff1953636ae005e4bd529ff52c4408d7b4dab voted for fa2091eda9d965657db8f7394bc772ac39e07552 1
2500:X 31 Aug 14:48:19.045 # +elected-leader master mymaster 127.0.0.1 6379
2500:X 31 Aug 14:48:19.045 # +failover-state-select-slave master mymaster 127.0.0.1 6379
2500:X 31 Aug 14:48:19.116 # +selected-slave slave 127.0.0.1:6579 127.0.0.1 6579 @ mymaster 127.0.0.1 6379
2500:X 31 Aug 14:48:19.116 * +failover-state-send-slaveof-noone slave 127.0.0.1:6579 127.0.0.1 6579 @ mymaster 127.0.0.1 6379
2500:X 31 Aug 14:48:19.199 * +failover-state-wait-promotion slave 127.0.0.1:6579 127.0.0.1 6579 @ mymaster 127.0.0.1 6379
2500:X 31 Aug 14:48:19.489 # +promoted-slave slave 127.0.0.1:6579 127.0.0.1 6579 @ mymaster 127.0.0.1 6379
2500:X 31 Aug 14:48:19.489 # +failover-state-reconf-slaves master mymaster 127.0.0.1 6379
2500:X 31 Aug 14:48:19.570 * +slave-reconf-sent slave 127.0.0.1:6479 127.0.0.1 6479 @ mymaster 127.0.0.1 6379
2500:X 31 Aug 14:48:20.122 # -odown master mymaster 127.0.0.1 6379
2500:X 31 Aug 14:48:20.538 * +slave-reconf-inprog slave 127.0.0.1:6479 127.0.0.1 6479 @ mymaster 127.0.0.1 6379
2500:X 31 Aug 14:48:20.538 * +slave-reconf-done slave 127.0.0.1:6479 127.0.0.1 6479 @ mymaster 127.0.0.1 6379
2500:X 31 Aug 14:48:20.599 # +failover-end master mymaster 127.0.0.1 6379
2500:X 31 Aug 14:48:20.599 # +switch-master mymaster 127.0.0.1 6379 127.0.0.1 6579
2500:X 31 Aug 14:48:20.599 * +slave slave 127.0.0.1:6479 127.0.0.1 6479 @ mymaster 127.0.0.1 6579
2500:X 31 Aug 14:48:20.599 * +slave slave 127.0.0.1:6379 127.0.0.1 6379 @ mymaster 127.0.0.1 6579
从日志中就可以看到主观下线,超过3/2,开始失效转移(failover),选举,变更为6579.看一下配置文件,发现slaveod已经是6579端口的redis了。重新启动后,再看一下6379的信息:
127.0.0.1:6379> info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6579
master_link_status:down
master_last_io_seconds_ago:-1
master_sync_in_progress:0
slave_repl_offset:1
master_link_down_since_seconds:1598858335
slave_priority:100
slave_read_only:1
connected_slaves:0
master_repl_offset:0
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
发现他的role已经是slave了。
Redis Cluster集群
上面的模式实现了redis的高可用,如果数据量非常大,一台服务器已经无法满足使用了,主从模式就显得不足了。我们就需要对数据分片,将数据分散到多台redis服务器上。这就是redis的cluster集群模式。
redis cluster的设计
- cluster是一种去中心化的集群,每个节点保存数据和整个集群的状态,各个节点之间互相连接。
- 如果集群中超过半数的节点都检测到某个节点不可用,那么就会认为该节点失效。
- 客户端连接集群中的任意一个节点即可
- redis-cluster会将所有的物理节点映射到0-16383间的slot上,当需要在集群中放入一个key-value时,根据 CRC16(key) mod 16384的值,决定将一个key放到哪个桶中。
- 对于每一个物理节点也可以配置主从模式,某个节点失效,就可以转移到slave节点上
redis cluster搭建
集群中至少有奇数个节点,以三个节点为例,每个再增加一个slave,我们需要6个redis实例。
仍然是复制redis.conf出6份,修改:
daemonize yes #后台启动
port 7001 #修改端口号,从7001到7006
cluster-enabled yes #开启cluster,去掉注释
cluster-config-file nodes.conf #自动生成
cluster-node-timeout 15000 #节点通信时间
appendonly yes #持久化方式
分别新建这六个服务后启动。
接下来需要使用一个ruby脚本来讲这六个节点加入到cluster集群中。由于是使用的yum安装,并没有找到这个脚本在哪,所以就下载了一个redis压缩包,到src下找到redis-trib.rb这个文件。这个就是我们要使用的脚本。接下来安装
gem install redis
需要注意的是要安装ruby来运行redis的脚本文件来创建cluster集群。如果遇到ruby版本太低,升级可以参考https://www.jianshu.com/p/a1a4d59490d7
./redis-trib.rb create --replicas 1 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 127.0.0.1:7000
--replicas 1 参数表示为每个主节点创建一个从节点,其他参数是实例的地址集合。
==安装过程中的坑==
- 如果你的conf中设置了密码,那么需要修改一个文件:/usr/local/rvm/gems/ruby-2.6.5/gems/redis-4.2.1/lib/redis/client.rb. 找到里面default里password的配置,配置成你定义的密码
- 如果出现Node is not empty. Either the node already knows other nodes的错误,需要我们清除一下node.conf 及aod,rdb的备份文件。文件路径是在conf配置中dir配置的。我的是在/var/lib/redis下的。然后分别登陆每一个节点。分别执行 flushdb
- 如果出现有一个槽已经被占用,分别登陆每一个节点,执行flushall,cluster reset命令
- 如果你是用的是jediscluster连接的集群,出现了JedisClusterMaxAttemptsException: No more cluster attempts left。在检查集群正常的情况下,可能是ip关系。jedis使用的是公网ip,而我们使用redis脚本指定的是内网ip,就会出现上面的情况。我们需要使用公网ip重新设置集群。注意redis.conf中bind ip的配置,会限制访问ip的,我们需要注销掉。
cluster集群的扩容 https://www.cnblogs.com/yfacesclub/p/11860927.html
分片与一致性hash
数据存取:
现在我们是三个主节点分别是:A, B, C 三个节点,它们可以是一台机器上的三个端口,也可以是三台不同的服务器。那么,采用哈希槽 (hash slot)的方式来分配16384个slot 的话,它们三个节点分别承担的slot 区间是:
节点A覆盖0-5460;
节点B覆盖5461-10922;
节点C覆盖10923-16383.
如果存入一个值,按照redis cluster哈希槽的算法: CRC16('key')384 = 6782。 那么就会把这个key 的存储分配到 B 上了。同样,当我连接(A,B,C)任何一个节点想获取'key'这个key时,也会这样的算法,然后内部跳转到B节点上获取数据。
看一下槽的分配情况:
127.0.0.1:5004> cluster nodes
ff22d6799b1731661fc158019af29c94bcb8293e 127.0.0.1:5002 master - 0 1598945599575 2 connected 5461-10922
55cec2805497dc43209792bc2e492e4184593ec4 127.0.0.1:5005 slave ff22d6799b1731661fc158019af29c94bcb8293e 0 1598945598572 5 connected
3e4c9d461d39acd684531fc6743e9bb8b946a0bb 127.0.0.1:5004 myself,slave 00404f89f54eb594a7758b5179b7d5e1ea6a21fc 0 0 4 connected
abf5bfdcca6b24faad0bcc4f4368a5f98dca49b1 127.0.0.1:5006 slave b62faa0c70604f1d39acfd5fd790fee2b123a1ed 0 1598945601578 6 connected
00404f89f54eb594a7758b5179b7d5e1ea6a21fc 127.0.0.1:5001 master - 0 1598945597069 1 connected 0-5460
b62faa0c70604f1d39acfd5fd790fee2b123a1ed 127.0.0.1:5003 master - 0 1598945602580 3 connected 10923-16383
如果想要扩容新增一个节点呢? 新增一个节点D,redis cluster的这种做法是从各个节点的前面各拿取一部分slot到D上。大致就会变成这样:
节点A覆盖1365-5460
节点B覆盖6827-10922
节点C覆盖12288-16383
节点D覆盖0-1364,5461-6826,10923-12287
这样就会按照新的槽位规则来存取了。
redis分片带来的问题
如上面的所示,当我们新增或者删除一个节点的时候,都会重新分配槽,同时还会产生数据的迁移。如果每台服务器中数据量都比较大,那么数据迁移工作是非常耗时的。
一致性hash算法
- 首先算出缓存节点的hash值,并将其配置到0~2^32的圆上
- 用同样的方法算出存储数据的key的哈希值,并映射到相同的圆上
-
然后从数据映射的位置开始顺时针查找,将数据映射到找到的第一个服务器上。
-
如果我们新增一个节点
从图片中我们发现node1,node2,node3的数据是不受影响的.node5插入后一部分属于node4的数据现在是计入node5上了,如果我们不做数据迁移,那么这一部分缓存就会失效,但是对于整个缓存系统系统而言,已经是将损失降到了最低。
- 一致性hash也会带来数据偏移的问题。尤其是当我们系统中节点较少时,可能会发生大量的数据都命中到一台服务器上。