Redis哨兵模式

redis哨兵模式

为什么要用redis哨兵模式:
————————————————————————————-
哨兵(Sentinel)主要是为了解决在主从复制架构中出现宕机的情况,主要分为两种情况:

1. 从Redis宕机

在Redis中从库重新启动后会自动加入到主从架构中,自动完成同步数据。在Redis2.8版本后,主从断线后恢复的情况下实现增量复制。

2. 主Redis宕机

需要以下2步才能完成

第一步:在从数据库中执行SLAVEOF NO ONE命令,断开主从关系并且提升为主库继续服务
第二步:将主库重新启动后,执行SLAVEOF命令,将其设置为其他库的从库,这时数据就能更新回来
由于这个手动完成恢复的过程其实是比较麻烦的并且容易出错,所以Redis提供的哨兵(sentinel)的功能来解决

————————————————————————————-

什么是redis哨兵模式:

Redis-Sentinel是用于管理Redis集群,该系统执行以下三个任务:
————————————————————————————-
1.监控(Monitoring):Sentinel会不断地检查你的主服务器和从服务器是否运作正常
2.提醒(Notification):当被监控的某个Redis服务器出现问题时,Sentinel可以通过API向管理员或者其他应用程序发送通知
3.自动故障迁移(Automatic failover):当一个主服务器不能正常工作时,Sentinel 会开始一次自动故障迁移操作,它会将失效主服务器的其中一个从服务器升级为新的主服务器,并让失效主服务器的其他从服务器改为复制新的主服务器;当客户端试图连接失效的主服务器时,集群也会向客户端返回新主服务器的地址,使得集群可以使用新主服务器代替失效服务器
————————————————————————————-

1. 单哨兵模式

进入redis安装目录,修改sentinel配置文件sentinel.conf

vim sentinel.conf
sentinel monitor mymaster 127.0.0.1 6380 1

解释:设置 sentinel monitor 为Master 地址,后面的数字1,表示最低通过票数;

新启动一个ssh连接,来启动查看sentinel的状态

启动哨兵

./src/redis-sentinel ../sentinel.conf

强制杀掉master的进程

[root@test3 ~]# kill 50668
[root@test3 ~]# 50668:signal-handler (1513352001) Received SIGTERM scheduling shutdown...
50668:M 15 Dec 23:33:21.984 # User requested shutdown...
50668:M 15 Dec 23:33:21.984 * Saving the final RDB snapshot before exiting.
50668:M 15 Dec 23:33:21.987 * DB saved on disk
50668:M 15 Dec 23:33:21.987 * Removing the pid file.
50668:M 15 Dec 23:33:21.987 # Redis is now ready to exit, bye bye...

等待半分钟,再将6380启动起来

sentinel的状态如下:

[root@test3 src]# redis-sentinel  ../sentinel.conf 
50718:X 15 Dec 22:41:36.711 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
50718:X 15 Dec 22:41:36.711 # Redis version=4.0.2, bits=64, commit=00000000, modified=0, pid=50718, just started
50718:X 15 Dec 22:41:36.711 # Configuration loaded
50718:X 15 Dec 22:41:36.712 * Increased maximum number of open files to 10032 (it was originally set to 1024).
                _._                                                  
           _.-``__ ''-._                                             
      _.-``    `.  `_.  ''-._           Redis 4.0.2 (00000000/0) 64 bit
  .-`` .-```.  ```/    _.,_ ''-._                                   
 (    '      ,       .-`  | `,    )     Running in sentinel mode
 |`-._`-...-` __...-.``-._|'` _.-'|     Port: 26379
 |    `-._   `._    /     _.-'    |     PID: 50718
  `-._    `-._  `-./  _.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |           http://redis.io        
  `-._    `-._`-.__.-'_.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |                                  
  `-._    `-._`-.__.-'_.-'    _.-'                                   
      `-._    `-.__.-'    _.-'                                       
          `-._        _.-'                                           
              `-.__.-'                                               

# 哨兵ID
50718:X 15 Dec 22:41:36.715 # Sentinel ID is 777d65d1a6d22405689907e55518dcb00cf52df5
# 给master添加了一个监控,名为mymaster
50718:X 15 Dec 22:41:36.715 # +monitor master mymaster 127.0.0.1 6380 quorum 1
# 发现了下面2个slave节点
50718:X 15 Dec 22:41:36.716 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @ mymaster 127.0.0.1 6380
50718:X 15 Dec 22:41:36.717 * +slave slave 127.0.0.1:6382 127.0.0.1 6382 @ mymaster 127.0.0.1 6380

# SDOWN:subjectively down,直接翻译的为”主观”失效,即当前sentinel实例认为某个redis服务为”不可用”状态.
50718:X 15 Dec 22:44:14.693 # +sdown master mymaster 127.0.0.1 6380
50718:X 15 Dec 22:44:14.693 # +odown master mymaster 127.0.0.1 6380 #quorum 1/1
50718:X 15 Dec 22:44:14.693 # +new-epoch 1
# try-failover尝试故障转移
50718:X 15 Dec 22:44:14.693 # +try-failover master mymaster 127.0.0.1 6380
# 投票选举哨兵leader,当前leader ID为777d65d1a6d22405689907e55518dcb00cf52df5
50718:X 15 Dec 22:44:14.696 # +vote-for-leader 777d65d1a6d22405689907e55518dcb00cf52df5 1
# 选中leader
50718:X 15 Dec 22:44:14.696 # +elected-leader master mymaster 127.0.0.1 6380
# 选中其中一个slave作为master
50718:X 15 Dec 22:44:14.696 # +failover-state-select-slave master mymaster 127.0.0.1 6380
# 选中6382为master
50718:X 15 Dec 22:44:14.780 # +selected-slave slave 127.0.0.1:6382 127.0.0.1 6382 @ mymaster 127.0.0.1 6380
# 发送slaveof no one命令
50718:X 15 Dec 22:44:14.780 * +failover-state-send-slaveof-noone slave 127.0.0.1:6382 127.0.0.1 6382 @ mymaster 127.0.0.1 6380
# 等待升级master
50718:X 15 Dec 22:44:14.871 * +failover-state-wait-promotion slave 127.0.0.1:6382 127.0.0.1 6382 @ mymaster 127.0.0.1 6380
# 升级6382为master
50718:X 15 Dec 22:44:15.256 # +promoted-slave slave 127.0.0.1:6382 127.0.0.1 6382 @ mymaster 127.0.0.1 6380
# 故障转移状态切换到reconf-slaves
50718:X 15 Dec 22:44:15.256 # +failover-state-reconf-slaves master mymaster 127.0.0.1 6380
# 6380正在设置新的master
50718:X 15 Dec 22:44:15.324 * +slave-reconf-sent slave 127.0.0.1:6381 127.0.0.1 6381 @ mymaster 127.0.0.1 6380
# 把6381的master配置由原来的6380更新
50718:X 15 Dec 22:44:16.298 * +slave-reconf-inprog slave 127.0.0.1:6381 127.0.0.1 6381 @ mymaster 127.0.0.1 6380
50718:X 15 Dec 22:44:16.298 * +slave-reconf-done slave 127.0.0.1:6381 127.0.0.1 6381 @ mymaster 127.0.0.1 6380
# 故障恢复完成
50718:X 15 Dec 22:44:16.381 # +failover-end master mymaster 127.0.0.1 6380
# master从6380转为6382
50718:X 15 Dec 22:44:16.381 # +switch-master mymaster 127.0.0.1 6380 127.0.0.1 6382
#下面2行分别添加6381 6380为6382的slave
50718:X 15 Dec 22:44:16.381 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @ mymaster 127.0.0.1 6382
50718:X 15 Dec 22:44:16.381 * +slave slave 127.0.0.1:6380 127.0.0.1 6380 @ mymaster 127.0.0.1 6382
50718:X 15 Dec 22:44:41.158 * +convert-to-slave slave 127.0.0.1:6380 127.0.0.1 6380 @ mymaster 127.0.0.1 6382

查看redis的状态:

127.0.0.1:6382> info replication
# Replication
role:master
connected_slaves:2
slave0:ip=127.0.0.1,port=6381,state=online,offset=605905,lag=1
slave1:ip=127.0.0.1,port=6380,state=online,offset=605905,lag=1
master_replid:ef51779d1bbd69b9d24f78b843f0e19d5b41b2de
master_replid2:f265eeea184220f5030b262217ec0c0f85655a6f
master_repl_offset:605905
second_repl_offset:10510
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:561
repl_backlog_histlen:605345

发现6382已经成为新的master,而6380和6381为slave

分别查看6380和6381的redis配置文件发现slaveof配置发生变化:

slaveof 127.0.0.1 6382 master均指向了6382

2. 哨兵集群

复制3份不同的sentinel配置文件:

sentinel.conf sentinel/sentinel26380.conf sentinel/sentinel26381.conf

配置文件内容样板如下:

port 26379
daemonize yes
protected-mode no
dir "/tmp/redis26379"
loglevel debug
logfile "/tmp/sentinel26379/sentinel26379.log"
sentinel monitor mymaster 127.0.0.1 6382 1
sentinel down-after-milliseconds mymaster 3000
sentinel parallel-syncs mymaster 1
sentinel failover-timeout mymaster 10000

配置文件解析:

———————————————————————————————————-
port 26379

sentinel监听端口

daemonize yes

Redis默认不是以守护进程的方式运行,可以通过该配置项修改,使用yes启用守护进程

dir “/tmp/redis26379”

运行目录

sentinel monitor mymaster 127.0.0.1 6382 1

sentinel监听master名称及端口,myster名称为自定义 ,数字1,表示,当有多少个sentinel认为master失效时,master才算真正失效;即投票数

sentinel down-after-milliseconds mymaster 3000

指定master无法连接的失效时间,单位是毫秒,默认为3秒

sentinel failover-timeout mymaster 10000

指定连接master的超时时间,10秒

sentinel parallel-syncs mymaster 1

指定了在failover主备切换时最多可以有多少个slave同时对新的master进行 同步,数字越小,完成failover所需的时间就越长,但是如果数字越大,就意味着越多的slave因为replication而不可用。可以通过将这个值设为 1 来保证每次只有一个slave 处于不能处理命令请求的状态

————————————————————————————————————-

启动sentinel

[root@test3 redis-4.0.2]# ./src/redis-sentinel sentinel.conf 
52935:X 18 Dec 19:21:03.794 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
52935:X 18 Dec 19:21:03.794 # Redis version=4.0.2, bits=64, commit=00000000, modified=0, pid=52935, just started
52935:X 18 Dec 19:21:03.794 # Configuration loaded

[root@test3 redis-4.0.2]# ./src/redis-sentinel sentinel/sentinel26380.conf 
52940:X 18 Dec 19:22:33.536 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
52940:X 18 Dec 19:22:33.536 # Redis version=4.0.2, bits=64, commit=00000000, modified=0, pid=52940, just started
52940:X 18 Dec 19:22:33.536 # Configuration loaded

[root@test3 redis-4.0.2]# ./src/redis-sentinel sentinel/sentinel26381.conf 
52945:X 18 Dec 19:22:36.728 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
52945:X 18 Dec 19:22:36.728 # Redis version=4.0.2, bits=64, commit=00000000, modified=0, pid=52945, just started
52945:X 18 Dec 19:22:36.728 # Configuration loaded

验证:

kill掉之前master(master为6382)的进程,然后查看sentinel输出日志:

# 查看进程号
ps aux |grep redis
root      50551  0.1  0.2 149352  4068 pts/0    Sl   12月15   7:40 /usr/local/src/redis-4.0.2/src/redis-server 127.0.0.1:6381
root      50683  0.1  0.2 147304  4164 pts/1    Sl   12月15   7:47 /usr/local/src/redis-4.0.2/src/redis-server 127.0.0.1:6382
root      50725  0.1  0.2 147304  4140 pts/1    Sl   12月15   7:30 /usr/local/src/redis-4.0.2/src/redis-server 127.0.0.1:6380
root      52936  0.3  0.1 145256  2388 ?        Ssl  19:21   0:00 ./src/redis-sentinel *:26379 [sentinel]
root      52941  0.3  0.1 145256  2384 ?        Ssl  19:22   0:00 ./src/redis-sentinel *:26380 [sentinel]
root      52946  0.3  0.1 145256  2320 ?        Ssl  19:22   0:00 ./src/redis-sentinel *:26381 [sentinel]
root      52951  0.0  0.0 112676   984 pts/0    R+   19:22   0:00 grep --color=auto redis

# kill掉master
[root@test3 redis-4.0.2]# kill 50683

# sentinel输出日志
[root@test3 redis-4.0.2]# 50551:S 18 Dec 19:23:43.583 # Connection with master lost.
50551:S 18 Dec 19:23:43.583 * Caching the disconnected master state.
50551:S 18 Dec 19:23:44.384 * Connecting to MASTER 127.0.0.1:6382
50551:S 18 Dec 19:23:44.385 * MASTER <-> SLAVE sync started
50551:S 18 Dec 19:23:44.385 # Error condition on socket for SYNC: Connection refused
50551:S 18 Dec 19:23:45.393 * Connecting to MASTER 127.0.0.1:6382
50551:S 18 Dec 19:23:45.393 * MASTER <-> SLAVE sync started
50551:S 18 Dec 19:23:45.393 # Error condition on socket for SYNC: Connection refused
50551:S 18 Dec 19:23:46.400 * Connecting to MASTER 127.0.0.1:6382
50551:S 18 Dec 19:23:46.401 * MASTER <-> SLAVE sync started
50551:S 18 Dec 19:23:46.401 # Error condition on socket for SYNC: Connection refused
50551:M 18 Dec 19:23:46.892 # Setting secondary replication ID to ef51779d1bbd69b9d24f78b843f0e19d5b41b2de, valid up to offset: 45804038. New replication ID is 58f257d0ff3b4dd1aa6c507c1d0760213205db97
50551:M 18 Dec 19:23:46.892 * Discarding previously cached master state.
50551:M 18 Dec 19:23:46.893 * MASTER MODE enabled (user request from 'id=18 addr=127.0.0.1:48044 fd=10 name=sentinel-d15ec52e-cmd age=73 idle=0 flags=x db=0 sub=0 psub=0 multi=3 qbuf=0 qbuf-free=32768 obl=36 oll=0 omem=0 events=r cmd=exec')
50551:M 18 Dec 19:23:46.897 # CONFIG REWRITE executed with success.
50551:M 18 Dec 19:23:47.412 * Replication backlog freed after 3600 seconds without connected slaves.
50551:M 18 Dec 19:23:48.071 * Slave 127.0.0.1:6380 asks for synchronization
50551:M 18 Dec 19:23:48.071 * Unable to partial resync with slave 127.0.0.1:6380 for lack of backlog (Slave request was: 45804038).
50551:M 18 Dec 19:23:48.071 * Starting BGSAVE for SYNC with target: disk
50551:M 18 Dec 19:23:48.072 * Background saving started by pid 52953
52953:C 18 Dec 19:23:48.077 * DB saved on disk
52953:C 18 Dec 19:23:48.078 * RDB: 0 MB of memory used by copy-on-write
50551:M 18 Dec 19:23:48.116 * Background saving terminated with success
50551:M 18 Dec 19:23:48.116 * Synchronization with slave 127.0.0.1:6380 succeeded

# 重新开始选举master,并指定
[root@test3 redis-4.0.2]# 52985:S 18 Dec 19:31:45.500 * Before turning into a slave, using my master parameters to synthesize a cached master: I may be able to synchronize with the new master with just a partial transfer.
52985:S 18 Dec 19:31:45.500 * SLAVE OF 127.0.0.1:6382 enabled (user request from 'id=2 addr=127.0.0.1:49012 fd=7 name=sentinel-de5fe35f-cmd age=10 idle=0 flags=x db=0 sub=0 psub=0 multi=3 qbuf=0 qbuf-free=32768 obl=36 oll=0 omem=0 events=r cmd=exec')
52985:S 18 Dec 19:31:45.507 # CONFIG REWRITE executed with success.
52985:S 18 Dec 19:31:45.526 * Connecting to MASTER 127.0.0.1:6382
52985:S 18 Dec 19:31:45.527 * MASTER <-> SLAVE sync started
52985:S 18 Dec 19:31:45.527 * Non blocking connect for SYNC fired the event.
52985:S 18 Dec 19:31:45.529 * Master replied to PING, replication can continue...
52985:S 18 Dec 19:31:45.530 * Trying a partial resynchronization (request bd99ddc9c78acc08fd33fa3ad85b47b77a7313a6:1).
52956:M 18 Dec 19:31:45.530 * Slave 127.0.0.1:6381 asks for synchronization
52956:M 18 Dec 19:31:45.530 * Partial resynchronization not accepted: Replication ID mismatch (Slave asked for 'bd99ddc9c78acc08fd33fa3ad85b47b77a7313a6', my replication IDs are '9d0c14e8fb55532f12c861e982f75390d4b35164' and 'd4399da2538dc650b92fe019c3f1e2292c1493b3')
52956:M 18 Dec 19:31:45.530 * Starting BGSAVE for SYNC with target: disk
52956:M 18 Dec 19:31:45.531 * Background saving started by pid 52992
52985:S 18 Dec 19:31:45.532 * Full resync from master: 9d0c14e8fb55532f12c861e982f75390d4b35164:45896676
52985:S 18 Dec 19:31:45.532 * Discarding previously cached master state.
52992:C 18 Dec 19:31:45.537 * DB saved on disk
52992:C 18 Dec 19:31:45.539 * RDB: 0 MB of memory used by copy-on-write
52956:M 18 Dec 19:31:45.616 * Background saving terminated with success
52956:M 18 Dec 19:31:45.616 * Synchronization with slave 127.0.0.1:6381 succeeded
52985:S 18 Dec 19:31:45.616 * MASTER <-> SLAVE sync: receiving 178 bytes from master
52985:S 18 Dec 19:31:45.616 * MASTER <-> SLAVE sync: Flushing old data
52985:S 18 Dec 19:31:45.616 * MASTER <-> SLAVE sync: Loading DB in memory
52985:S 18 Dec 19:31:45.616 * MASTER <-> SLAVE sync: Finished with success

由上面的日志可以看出,6381被指定为新的master,再次将6382启动起来,查看6381的info信息如下:

127.0.0.1:6382> INFO replication
# Replication
role:master
connected_slaves:2
slave0:ip=127.0.0.1,port=6380,state=online,offset=45916803,lag=0
slave1:ip=127.0.0.1,port=6381,state=online,offset=45916803,lag=0
master_replid:9d0c14e8fb55532f12c861e982f75390d4b35164
master_replid2:d4399da2538dc650b92fe019c3f1e2292c1493b3
master_repl_offset:45916803
second_repl_offset:45889031
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:45813331
repl_backlog_histlen:103473 

Linux安装Redis

1. 基本知识

Redis 的数据类型

  字符串 , 列表 (lists) , 集合 (sets) , 有序集合 (sorts sets) , 哈希表 (hashs)

Redis 和 memcache 相比的独特之处

  • redis可以用来做存储 (storge) , 而memcache是来做缓存 (cache) 。这个特点主要是因为其有 “持久化” 功能
  • 存储的数据有 “结构” , 对于memcache来说 , 存储的数据 , 只有一种类型—— “字符串” , 而redis则可以存储字符串 , 链表 , 集合 , 有序集合 , 哈序结构

持久化的两种方式

Redis 将数据存储于内存中 , 或被配置为使用虚拟内存 ; 实现数据持久化的两种方式 :

  • 使用截图的方式 , 将内存中的数据不断写入磁盘 (性能高 , 但可能会引起一定程度的数据丢失)
  • 使用类似 MySql l的方式 , 记录每次更新的日志

Redis 服务端的默认端口是 6379

2. 安装 Redis

先去官网下载 Redis 安装包 https://redis.io/download

放到 /usr/local/app_pack 目录下面 , 解压文件 :

# tar -zxvf redis-4.0.6.tar.g

进入到解压后的目录 :

# cd redis-4.0.6

编译程序 :

# make

安装到指定目录 :

# make install PREFIX=/usr/local/redis

这时候 Redis 程序已经被安装到 /usr/local/redis/bin 目录
将配置文件移动到安装目录 :

# mv redis.conf /usr/local/redis/etc/redis.conf

启动 Redis 服务器 :

# /usr/local/redis/bin/redis-server /usr/local/redis/etc/redis.conf

这里是用 /usr/local/redis/etc/redis.conf 配置文件启动 Redis
关闭 Redis 服务器 :

# /usr/local/redis/bin/redis-cli shutdown

或者 # pkill redis-server

客户端连接 :

# /usr/local/redis/bin/redis-cli

设置一个 Redis 键值对 :

127.0.0.1:6379> set foo bar
OK

根据键获取值 :

127.0.0.1:6379> get foo
"bar"

默认情况 Redis 不是在后台运行 , 如果需要把 Redis 放在后台运行 , 编辑配置文件 :

# vim /usr/local/redis/etc/redis.conf

将 daemonize 的值改为 yes

让 Redis 开机启动 :

# vim /etc/rc.local

加入

/usr/local/redis/bin/redis-server /usr/local/redis/etc/redis-conf

3. 文件及配置概述

/usr/local/redis/bin 目录下的几个文件用途 :

redis-benchmark : Redis 性能测试工具
redis-check-aof : 检查 aof 日志的工具
redis-check-dump : 检查 rdb 日志的工具
redis-cli : 连接用的客户端
redis-server : Redis 服务进程

Redis 的配置项解析 :

daemonize : 如需要在后台运行 , 把该项的值改为 yes
pdifile : 把 pid 文件放在 /var/run/redis.pid , 可以配置到其他地址
bind : 指定 Redis 只接收来自该 IP 的请求 , 如果不设置 , 那么将处理所有请求 , 在生产环节中最好设置该项
port : 监听端口 , 默认为 6379
timeout : 设置客户端连接时的超时时间 , 单位为秒
loglevel : 等级分为 4 级 , debug , revbose , notice 和 warning , 生产环境下一般开启 notice
logfile : 配置 log 文件地址 , 默认使用标准输出 , 即打印在命令行终端的端口上
database : 设置数据库的个数 , 默认使用的数据库是 0
save : 设置 redis 进行数据库镜像的频率
rdbcompression : 在进行镜像备份时 , 是否进行压缩
dbfilename : 镜像备份文件的文件名
dir : 数据库镜像备份的文件放置的路径
slaveof : 设置该数据库为其他数据库的从数据库
masterauth : 当主数据库连接需要密码验证时 , 在这里设定
requirepass : 设置客户端连接后进行任何其他指定前需要使用的密码
maxclients : 限制同时连接的客户端数量
maxmemory : 设置redis能够使用的最大内存
appendonly : 开启 appendonly 模式后 , Redis 会把每一次所接收到的写操作都追加到 appendonly.aof 文件中 , 当 Redis 重新启动时 , 会从该文件恢复出之前的状态
appendfsync : 设置 appendonly.aof 文件进行同步的频率
vm_enabled : 是否开启虚拟内存支持
vm_swap_file : 设置虚拟内存的交换文件的路径
vm_max_momery : 设置开启虚拟内存后 , Redis 将使用的最大物理内存的大小 , 默认为 0
vm_page_size : 设置虚拟内存页的大小
vm_pages : 设置交换文件的总的page数量
vm_max_thrrads : 设置 vm IO 同时使用的线程数量

4. 错误解决办法

  • 客户端连接时 , 提示 DENIED Redis is running in protected mode because protected mode is enabled…

将配置文件里的 protected mode改为了 no , 原本是 yes

Redis笔记(三)Redis持久化

持久化的作用

么是持久化

Redis的数据操作都在内存中,redis崩掉的话,会丢失。Redis持久化就是对数据的更新异步的保存在磁盘上,以便数据恢复。

持久化的实现方式

  • 快照方式
    对数据在某时某点的一种完整备份。例如Redis RDB,MySQL Dump都是这种方式。

  • 写日志方式
    任何数据的更新都记录在日志当中,某个时候要进行数据的恢复时,重走一遍日志的完整过程。例如MySQL的Binlog,HBase的HLog和Redis的AOF,就是这种方式。

RDB

什么是RDB

将Redis内存中的数据,完整的生成一个快照,以二进制格式文件(后缀RDB)保存在硬盘当中。当需要进行恢复时,再从硬盘加载到内存中。

Redis主从复制,用的也是基于RDB方式,做一个复制文件的传输。

三种触发方式

  • save命令触发方式(同步)
redis> save
OK

save执行时,会造成Redis的阻塞。所有数据操作命令都要排队等待它完成。
文件策略:新生成一个新的临时文件,当save执行完后,用新的替换老的。

  • bgsave命令触发方式(异步)
redis> bgsave
Background saving started

客户端对Redis服务器下达bgsave命令时,Redis会fork出一个子进程进行RDB文件的生成。当RDB生成完毕后,子进程再反馈给主进程。fork子进程时也会阻塞,不过正常情况下fork过程都非常快的。
文件策略:与save命令相同。

save与bgsave对比:

未分类

  • 规则自动触发方式

某些条件达到时,自动生成RDB文件。

比如我们配置如下:

未分类

以上任一条件达到时,都会触发生成RDB文件。不过这种方式对RDB文件的生成频率不太好控制。如果写量大,RDB生成会很频繁。不是一种好的方式。

修改配置文件:

# 配置自动生成规则。一般不建议配置自动生成RDB文件
save 900 1
save 300 10
save 60 10000
# 指定rdb文件名
dbfilename dump-${port}.rdb
# 指定rdb文件目录
dir /opt/redis/data
# bgsave发生错误,停止写入
stop-writes-on-bgsave-error yes
# rdb文件采用压缩格式
rdbcompression yes
# 对rdb文件进行校验
rdbchecksum yes

不容忽略的触发方式

  1. 全量复制
    主从复制时,主会自动生成RDB文件。

  2. debug reload
    Redis中的debug reload提供debug级别的重启,不清空内存的一种重启,这种方式也会触发RDB文件的生成。

  3. shutdown
    会触发RDB文件的生成。

试验

save试验

cd redis
cd config
cp ../redis.conf
cp redis.conf redis-6379.conf
vim redis-6379.conf

修改如下配置:

daemonize yes
pidfile /var/run/redis-6379.pid
port 6379
logfile "6379.log"
# 先关闭自动生成RDB的配置
# save 900 1
# save 300 10
# save 60 10000
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
dbfilename dump-6379.rdb
dir /opt/soft/redis/data

保存配置

:wq
# 重启redis
redis-server redis-6379.conf
# 客户端连接
redis-cli
# 暂时还没有Redis内存数据
127.0.0.1:6379> dbsize
(integer) 0

然后写个简单for循环程序,往Redis执行大量的写命令,让内存数据足够大。

# 再看一下,这下内存数据有很多了
127.0.0.1:6379> dbsize
(integer) 5000000
# 看下内存使用了904M,足够用以演示save的阻塞了
127.0.0.1:6379> info memory
used_memory: 948306016
used_memory_human: 904.38M
used_memory_rss: 1031897088
used_memory_peak: 981827104
used_memory_peak_human: 936.34M
used_memory_lua: 36864
mem_fragmentation_ratio: 1.09
mem_allocator: libc
# 执行save,发现等待了若干秒后,才输出OK以及消耗时间
127.0.0.1:6379> save
OK
(8.94s)

/opt/soft/redis/data目录下也会生成dump-6379.rdb文件。

bgsave试验

验证bgsave的非阻塞:

# 我们再开一个新窗口,在新窗口上连接redis客户端
redis-cli
# 输好以下命令,先别执行
127.0.0.1:6379> get hello
# 然后在原窗口执行bgsave
127.0.0.1:6379> bgsave
Background saving started
# 马上切回新窗口,回车执行命令,发现world即刻返回,验证了bgsave的非阻塞
127.0.0.1:6379> get hello
"world"

接下来验证bgsave会生成子进程:

# 在新窗口先查看下redis进程,过滤掉客户端和grep进程,发现就只有一个redis主进程
ps -ef | grep redis- | grep -v "redis-cli" | grep -v "grep"
501 36775     1   0 10:22下午 ??      0:17 .86 redis-server *:6379
# 在原窗口再执行一次bgsave
127.0.0.1:6379> bgsave
Background saving started
# 马上切新窗口再次查看进程,发现多了个子进程redis-rdb-bgsave
ps -ef | grep redis- | grep -v "redis-cli" | grep -v "grep"
501 36775     1   0 10:22下午 ??      0:17 .91 redis-server *:6379
501 36954     1   0 10:28下午 ??      0:02 .81 redis-rdb-bgsave *:6379
# 再看一次,子进程已经不在了。因为子进程已经完成了它生成rdb文件的工作
ps -ef | grep redis- | grep -v "redis-cli" | grep -v "grep"
501 36775     1   0 10:22下午 ??      0:17 .91 redis-server *:6379

最后验证文件策略:

# 在新窗口/data目录查看文件
ls
6379.log dump-6379.log
# 在原窗口再执行一次bgsave
127.0.0.1:6379> bgsave
Background saving started
# 切新窗口再次查看文件,多了个临时的rdb文件
ls
6379.log dump-6379.rdb temp-36985.rdb
# 过会儿再查看一次,临时文件消失了
ls
6379.log dump-6379.rdb

自动生成试验

这个不演示了,自己修改配置文件save 60 5,配置60秒更新5次就自动生成RDB文件。重启redis后,我们在客户端用set命令执行5次。观察/data下的rdb文件的时间戳是否变化了来验证。

我们也可以查看下日志文件6379.log,输出了试验过程的相关日志内容。

RDB总结

  1. RDB是Redis内存到硬盘的快照,用于持久化;

  2. save通常会阻塞redis;

  3. bgsave通常不会阻塞redis,但是会fork新进程;

  4. save自动配置满足任一就会被执行;

  5. 有些触发机制不容忽视。

AOF

RDB存在的问题

  • 耗时、耗内存、耗IO性能
    将内存中的数据全部dump到硬盘当中,耗时。bgsave的方式fork()子进程耗额外内存。大量的硬盘读写耗费IO性能。

  • 不可控、丢失数据
    宕机时,上次快照之后写入的内存数据,将会丢失。

什么是AOF

就是写日志,每次执行Redis写命令,让命令同时记录日志(以AOF日志格式)。Redis宕机时,只要进行日志回放就可以恢复数据。

AOF三种策略

首先Redis执行写命令,将命令刷新到硬盘缓冲区当中。

  • always
    always策略让缓冲区中的数据即时刷新到硬盘。

  • everysec
    everysec策略让缓冲区中的数据每秒刷新到硬盘。相比always,在高写入量的情况下,可以保护硬盘。出现故障可能会丢失一秒数据。

  • no
    刷新策略让操作系统来决定。

三种策略对比

未分类

通常使用everysec策略,这也是AOF的默认策略。

AOF重写

随着时间的推移,命令的逐步写入。AOF文件也会逐渐变大。当我们用AOF来恢复时会很慢,而且当文件无限增大时,对硬盘的管理,对写入的速度也会有产生影响。Redis当然考虑到这个问题,所以就有了AOF重写。

原生AOF:

set hello world
set hello java
set hello python
incr counter
incr counter
rpush mylist a
rpush mylist b
rpush mylist c
过期数据

重写后的AOF:

set hello python
set incr 2
rpush mylist a b c

AOF重写就是把过期的、没用的、重复的以及可优化的命令,进行化简。只取最终有价值的结果。虽然写入操作很频繁,但系统定义的key的量是相对有限的。

AOF重写可以大大压缩最终日志文件的大小。从而减少磁盘占用量,加快数据恢复速度。比如我们有个计数的服务,有很多自增的操作,比如有一个key自增到1个亿,对AOF文件来说就是一亿次incr。AOF重写就只用记1条记录。

AOF重写两种方式

  • bgrewriteaof命令触发AOF重写
    redis客户端向Redis发bgrewriteaof命令,redis服务端fork一个子进程去完成AOF重写。这里的AOF重写,是将Redis内存中的数据进行一次回溯,回溯成AOF文件。而不是重写AOF文件生成新的AOF文件去替换。

  • AOF重写配置
    auto-aof-rewrite-min-size:AOF文件重写需要的尺寸
    auto-aof-rewrite-percentage:AOF文件增长率
    redis提供了aof_current_size和aof_base_size,分别用来统计AOF当前尺寸(单位:字节)和AOF上次启动和重写的尺寸(单位:字节)。
    AOF自动重写的触发时机,同时满足以下两点):

  • aof_current_size > auto-aof-rewrite-min-size
  • aof_current_size – aof_base_size/aof_base_size > auto-aof-rewrite-percentage

AOF重写配置

修改配置文件:

# 开启正常AOF的append刷盘操作
appendonly yes
# AOF文件名
appendfilename "appendonly-6379.aof"
# 每秒刷盘
appendfsync everysec
# 文件目录
dir /opt/soft/redis/data
# AOF重写增长率
auto-aof-rewrite-percentage 100
# AOF重写最小尺寸
auto-aof-rewrite-min-size 64mb
# AOF重写期间是否暂停append操作。AOF重写非常消耗磁盘性能,而正常的AOF过程中也会往磁盘刷数据。
# 通常偏向考虑性能,设为yes。万一重写失败了,这期间正常AOF的数据会丢失,因为我们选择了重写期间放弃了正常AOF刷盘。
no-appendfsync-on-rewrite yes

试验

redis-cli
127.0.0.1:6379> dbsize
(integer) 5000000
127.0.0.1:6379> exit
vim redis-6379.conf
appendonly yes
appendfilename "appendonly.aof"
appendfsync everysec
no-appendfsync-on-rewrite yes
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
aof-load-truncated yes

配置修改后,要重新启动redis。
appendonly是支持动态配置,不用重启Redis:

127.0.0.1:6379> config get appendonly
1) "appendonly"
2) "no"
127.0.0.1:6379> config set appendonly yes
OK
# 重新加载配置
127.0.0.1:6379> config rewrite
OK
127.0.0.1:6379> exit

先试验下正常AOF刷盘

# 客户端连接redis,执行一些命令:
redis-cli
127.0.0.1:6379> set hello world
OK
127.0.0.1:6379> set hello java
OK
127.0.0.1:6379> set hello redis
OK
127.0.0.1:6379> incr counter
(integer) 1
127.0.0.1:6379> incr counter
(integer) 2
127.0.0.1:6379> rpush list a
(integer) 1
127.0.0.1:6379> rpush list b
(integer) 2
127.0.0.1:6379> rpush list c
(integer) 3
127.0.0.1:6379> exit
# 我们查看data目录,appendonly.aof文件已经生成了
cd /opt/soft/redis/data
ll
-rw-r--r--   1   carlosfu  staff   16K  10  7  22:28  6379.log
-rw-r--r--   1   carlosfu  staff  243B  10  7  22:29  appendonly.aof
-rw-r--r--   1   carlosfu  staff   18B  10  7  22:19  dump-6379.rdb

再试验下AOF重写

redis-cli
127.0.0.1:6379> bgrewriteaof
Background append only file rewriteing started
127.0.0.1:6379> dbsize
(integer) 3
# 我们再查看data目录,appendonly.aof文件变小了
cd /opt/soft/redis/data
ll
-rw-r--r--   1   carlosfu  staff   17K  10  7  22:33  6379.log
-rw-r--r--   1   carlosfu  staff  137B  10  7  22:33  appendonly.aof
-rw-r--r--   1   carlosfu  staff   18B  10  7  22:19  dump-6379.rdb

RDB和AOF的抉择

RDB和AOF的比较

未分类

RDB最佳策略

  1. 建议关闭RDB
    无论是Redis主节点,还是从节点,都建议关掉RDB。但是关掉不是绝对的,主从复制时还是会借助RDB。

  2. 用作数据备份
    RDB虽然是很重的操作,但是对数据备份很有作用。文件大小比较小,可以按天或按小时进行数据备份。

  3. 主从,从开?
    在极个别的场景下,需要在从节点开RDB,可以再本地保存这样子的一个历史的RDB文件。虽然从节点不进行读写,但是Redis往往单机多部署,由于RDB是个很重的操作,所以还是会对CPU、硬盘和内存造成一定影响。根据实际需求进行设定。

AOF最佳策略

  1. 建议开启AOF
    如果Redis数据只是用作数据源的缓存,并且缓存丢失后从数据源重新加载不会对数据源造成太大压力,这种情况下。AOF可以关。

  2. AOF重写集中管理
    单机多部署情况下,发生大量fork可能会内存爆满。

  3. everysec
    建议采用每秒刷盘策略

最佳策略

  1. 小分片
    使用maxmemary对Redis最大内存进行规划。

  2. 缓存和存储
    根据缓存和存储的特性来决定使用哪种策略

  3. 监控(硬盘、内存、负载、网络)

  4. 足够的内存
    不要把就机器全部的内存规划给Redis。不然会出很多问题。像客户端缓冲区等,不受maxmemary限制。规划不当可能会产生SWAP、OOM等问题。

开发运维常见问题

fork操作

fork是一个同步操作。执行bgsave和bgrewriteaof时都会执行fork操作,

改善fork

  1. 优先使用物理机或者其他能高效支持form操作的虚拟化技术;

  2. 控制Redis实例最大可用内存maxmemary;
    fork操作只是执行内存页的拷贝,大部分情况速度是比较快的。redis内存越大,内存页越大。可以使用maxmemary规划redis内存,避免fork过慢。

  3. 合理配置Linux内存分配策略:vm.overcommit_memory=1
    fork时如果内存不够,会阻塞。Linux的vm.overcommit_memory默认为0,不会分配额外内存

子进程开销和优化

bgsave和bgrewriteaof会进行fork操作产生子进程。

  • CPU
    • 开销:RDB和AOF文件生成属于CPU密集型;
    • 优化:不做CPU绑定,不和CPU密集型应用部署在一起;
  • 内存
    • 开销:fork内存开销
    • 优化:echo never > /sys/kernel/mm/transparent_hugepage/enabled
  • 硬盘
    • 开销:AOF和RDB文件写入,可以结合iostat和iotao分析
    • 优化:
  1. 不要和高硬盘负载服务部署在一起:存储服务、消息队列;
  2. no-appendfsync-on-rewrite=yes;
  3. 根据写入量决定磁盘类型:例如sdd;
  4. 单机多实例持久化文件目录可以考虑分盘;

AOF追加阻塞

AOF阻塞定位

redis日志:

Asynchronous AOF fsync is taking to long(disk is busy?). Writing the AOF 
buffer whitout waiting for fsync to complete, this may slow down Redis

info persistence
可以查看上述日志发生的次数。

127.0.0.1:6379> info persistence
......
......
aof_delayed_fsync: 100
......
......

改善方式

同子进程的硬盘优化。

Redis笔记(二)Redis其他功能

慢查询

生命周期

未分类

两个配置

  • slowlog-max-len:慢查询队列长度
  • slowlog-log-slower-than:慢查询阈值(单位:微秒)
  • slowlog-log-slower-than=0, 记录所有命令
  • slowlog-log-slower-than<0, 不记录任何命令

配置方法

  • 默认值
    • config get slowlog-max-len = 128
    • config get slowlog-log-slower-than = 10000
  • 修改配置文件重启

  • 动态配置

    • config set slowlog-max-len 1000
    • config set slowlog-log-slower-than 1000

三个命令

  • slowlog get [n]:获取慢查询队列
  • slowlog len:获取慢查询队列长度
  • slowlog reset:清空慢查询队列

运维经验

  1. slowlog-max-len不要设置过大,默认10ms,通常设置1ms
    Redis的QPS是万级的,也就是一条命令平均0.1ms就执行结束了。通常我们翻10倍,设置1ms。

  2. slowlog-log-slower-than不要设置过小,通常设置1000左右
    慢查询队列默认长度128,超过的话,按先进先出,之前的慢查询会丢失。通常我们设置1000。

  3. 理解命令生命周期
    慢查询发生在第三阶段

  4. 定期持久化慢查询
    slowlog get或其他第三方开源工具

pipeline

什么是流水线

未使用pipeline的批量网络通信模型

未分类

假设客户端在上海,Redis服务器在北京。相距1300公里。假设光纤速度≈光速2/3,即30000公里/秒2/3。那么一次命令的执行时间就是(13002)/(300002/3)=13毫秒。Redis万级QPS,一次命令执行时间只有0.1毫秒,因此网络传输消耗13毫秒是不能接受的。在N次命令操作下,Redis的使用效率就不是很高了。

使用pipeline的网络通信模型

未分类

客户端实现

引入maven依赖

<dependency>
  <groupId>redis.clients</groupId>
  <artifacId>jedis</artifacId>
  <version>2.9.0</version>
  <type>jar</type>
  <scope>compile</scope>
</dependency>
// 不用pipeline
Jedis jedis = new Jedis("127.0.0.1", 6379);
for (int i = 0; i < 10000; i++) {
    jedis.hset("hashkey" + i, "field" + i, "value=" + i);
}

不用pipeline,10000次hset,总共耗时50s,不同网络环境可能有所不同

// 使用pipeline, 我们将10000条命令分100次pipeline,每次100条命令
Jedis jedis = new Jedis("127.0.0.1", 6379);
for (int i = 0; i < 100; i++) {
    Pipeline pipeline = jedis.pipeline();
    for (int j = i * 100; j < (i + 1) * 100; j++) {
        pipeline.hset("hashkey:" + j, "field" + j, "value" + j);
    }
    pipeline.syncAndReturnAll();
}

使用pipelne,10000次hset,总共耗时0.7s,不同网络环境可能有所不同。

可见在执行批量命令时,使用pipeline对Redis的使用效率提升是非常明显的。

与M原生操作对比

mset、mget等操作是原子性操作,一次m操作只返回一次结果。
pipeline非原子性操作,只是将N次命令打个包传输,最终命令会被逐条执行,客户端接收N次返回结果。

pipeline使用建议

  1. 注意每次pipeline携带数据量
    pipeline主要就是压缩N次操作的网络时间。但是pipeline的命令条数也不建议过大,避免单次传输数据量过大,客户端等待过长。

  2. Redis集群中,pipeline每次只作用在一个Reids节点上

发布订阅

角色

  • 发布者(publisher)
  • 订阅者(subscriber)
  • 频道(channel)

模型

未分类

订阅者可以订阅多个频道

API

发布

API:publish channel message

redis> publish sohu:tv "hello world"
(integer) 3 #订阅者个数
redis> publish sohu:auto "taxi"
(integer) #没有订阅者

订阅

API:subscribe [channel] #一个或多个

redis> subscribe sohu:tv
1) "subscribe"
2) "sohu:tv"
3) (integer) 1
1) "message"
2) "sohu:tv"
3) "hello world"

取消订阅

API:unsubscribe [channel] #一个或多个

redis> unsubscribe sohu:tv
1) "unsubscribe"
2) "sohu:tv"
3) (integer) 0

其他API

  • psubscribe [pattern…] #订阅指定模式
  • punsubscribe [pattern…] #退订指定模式
  • pubsub channels #列出至少有一个订阅者的频道
  • pubsub numsubs [channel…] #列出给定频道的订阅者数量

对比消息队列

消息队列模型

未分类

发布订阅模型,订阅者均能收到消息。消息队列,只有一个订阅者能收到消息。因此使用发布订阅还是消息队列,要搞清楚使用场景。

geo

geo是什么

GEO:存储经纬度,计算两地距离,范围计算等。

相关命令

API:geoadd key longitude latitude member # 增加地理位置信息

redis> geoadd cities:locations 116.28 39.55 bejing
(integer) 1
redis> geoadd cities:locations 117.12 39.08 tianjin 114.29 38.02 shijiazhuang 118.01 39.38 tangshan 115.29 38.51 baoding
(integer) 4

API:geopos key member [member…] # 获取地理位置信息

redis> geopos cities:locations tianjin
1)1) "117.12000042200088501"
  2) "39.0800000535766543"

API:geodist key member1 member2 [unit] # 获取两位置距离,unit:m(米)、km(千米)、mi(英里)、ft(尺)

reids> geodist cities:locations tianjin beijing km
"89.2061"

API:
获取指定位置范围内的地理位置信息集合
georadius key longitude latitude radius m|km|ft|mi [withcoord] [withdist] [withhash] [COUNT count] [asc|desc] [store key] [storedist key]

georadiusbymember key member radius m|km|ft|mi [withcoord] [withdist] [withhash] [COUNT count] [asc|desc] [store key] [storedist key]

withcoord: 返回结果中包含经纬度
withdist: 返回结果中包含距离中心节点的距离
withhash: 返回结果中包含geohash
COUNT count:指定返回结果的数量
asc|desc:返回结果按照距离中心节点的距离做升序/降序
store key:将返回结果的地理位置信息保存到指定键
storedist key:将返回结果距离中心点的距离保存到指定键

redis> georadiusbymember cities:locations beijing 150 km
1)"beijing"
2) "tianjin"
3) "tangshan"
4) "baoding"

相关说明

  1. since redis3.2+

  2. redis geo是用zset实现的,type geokey = zset

  3. 没有删除API,可以使用zset的API:zrem key member

Redis系列-Redis笔记(一)

Redis基础

Redis安装

# 下载
cd /tmp
wget http://download.redis.io/releases/redis-3.2.11.tar.gz
# 解压
tar -zxvf redis-3.2.11.tar.gz
# 建立软连接
ln -s redis-3.2.11 redis
# 编译安装
cd redis
make
make install
# 查看redis-开头的可执行文件
cd src
ll | grep redis-
# 显示如下
-rwxr-xr-x 1 root root 2432576 Nov 25 18:00 redis-benchmark
-rw-rw-r-- 1 root root   29559 Sep 21 22:20 redis-benchmark.c
-rw-r--r-- 1 root root  108448 Nov 25 18:00 redis-benchmark.o
-rwxr-xr-x 1 root root   25168 Nov 25 18:00 redis-check-aof
-rw-rw-r-- 1 root root    6328 Sep 21 22:20 redis-check-aof.c
-rw-r--r-- 1 root root   33688 Nov 25 18:00 redis-check-aof.o
-rwxr-xr-x 1 root root 5191328 Nov 25 18:00 redis-check-rdb
-rw-rw-r-- 1 root root   12789 Sep 21 22:20 redis-check-rdb.c
-rw-r--r-- 1 root root   54496 Nov 25 18:00 redis-check-rdb.o
-rwxr-xr-x 1 root root 2585448 Nov 25 18:00 redis-cli
-rw-rw-r-- 1 root root   90678 Sep 21 22:20 redis-cli.c
-rw-r--r-- 1 root root  366344 Nov 25 18:00 redis-cli.o
-rwxr-xr-x 1 root root 5191328 Nov 25 18:00 redis-sentinel
-rwxr-xr-x 1 root root 5191328 Nov 25 18:00 redis-server
-rwxrwxr-x 1 root root   60852 Sep 21 22:20 redis-trib.rb
# 回到redis目录
cd ..
# 启动redis
redis-server
# 可以用下面三种方式验证redis服务器是否正常
# 查询redis进程
ps -ef | grep redis
root     30253 27126  0 18:10 pts/0    00:00:00 redis-server *:6379
root     30279 30259  0 18:19 pts/1    00:00:00 grep --color=auto redis
# 查询redis端口占用
netstat -antpl | grep redis
tcp        0      0 0.0.0.0:6379            0.0.0.0:*               LISTEN      30253/redis-server
tcp6       0      0 :::6379                 :::*                    LISTEN      30253/redis-server
# 用redis客户端进行连接,然后执行ping,返回PONG
redis-cli -h 127.0.0.1 -p 6379
127.0.0.1:6379> ping
PONG

Redis启动方式

  • 直接启动:redis-server
  • 动态参数启动:redis-server –port 6380
  • 指定配置文件启动:redis-server configPath

生产环境建议指定配置文件启动,单机多实例配置文件可以用端口区分开,比如像下面这样:

mv redis.conf redis-6380.conf
vim redis-6380.conf

暂时我们仅指定以下几项基础配置:

# 是否以守护进程方式启动
daemonize yes
# redis对外端口
port 6380
# 工作目录
dir ./
# 日志文件
logfile "6380.log"

用指定配置文件方式重新启动Redis

redis-server redis-6380.conf
ps -ef | grep redis
root     30339     1  0 18:55 ?        00:00:00 redis-server 127.0.0.1:6380
root     30344 30259  0 18:56 pts/1    00:00:00 grep --color=auto redis

Redis客户端

redis-cli连接

# 我们在任意目录下启动redis客户端
redis-cli -h 127.0.0.1 -p 6379
127.0.0.1:6379> ping
PONG
127.0.0.1:6379> incr number
(integer) 1
127.0.0.1:6379> set hello world
OK
127.0.0.1:6379> set foo bar
OK
127.0.0.1:6379> get hello
"world"
127.0.0.1:6379>

redis-cli返回值

  • 状态回复
127.0.0.1:6379> ping
PONG
  • 错误回复
127.0.0.1:6379> hget hello
(error) ERR wrong number of arguments for 'hget' command
  • 整数回复
127.0.0.1:6379> incr number
(integer) 1
  • 字符串回复
127.0.0.1:6379> set hello world
OK
127.0.0.1:6379> get hello
"world"
  • 多行字符串回复
127.0.0.1:6379> set foo bar
OK
127.0.0.1:6379> mget hello foo
1) "world"
2) "bar"

Redis基础配置

  • daemonize:是否守护进程(no|yes)
  • port:Redis对外端口
  • logfile:Redis系统日志
  • dir:Redis工作目录

除了上面几个最基本的配置,redis还有下面这些高级配置:

  1. RDB config
  2. AOF config
  3. show Log config
    maxMemory等等
    后续穿插讲解

可以通过以下命令列出配置:

# 列出配置文件的配置项,去除注释行和空行
cat redis.conf | grep -v '#' | grep -v '^$'
bind 127.0.0.1
protected-mode yes
port 6379
tcp-backlog 511
timeout 0
tcp-keepalive 300
daemonize yes
supervised no
pidfile /var/run/redis_6379.pid
loglevel notice
logfile ""
databases 16
save 900 1
save 300 10
save 60 10000
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
dbfilename dump.rdb
dir ./
slave-serve-stale-data yes
slave-read-only yes
repl-diskless-sync no
repl-diskless-sync-delay 5
repl-disable-tcp-nodelay no
slave-priority 100
appendonly no
appendfilename "appendonly.aof"
appendfsync everysec
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
aof-load-truncated yes
lua-time-limit 5000
slowlog-log-slower-than 10000
slowlog-max-len 128
latency-monitor-threshold 0
notify-keyspace-events ""
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
list-max-ziplist-size -2
list-compress-depth 0
set-max-intset-entries 512
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
hll-sparse-max-bytes 3000
activerehashing yes
client-output-buffer-limit normal 0 0 0
client-output-buffer-limit slave 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60
hz 10
aof-rewrite-incremental-fsync yes

Redis API

通用命令

  • keys [pattern]:显示通配符匹配的key
  • dbsize:查看key总数
127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3 k4 v4
OK
127.0.0.1:6379> dbsize
(integer) 4
127.0.0.1:6379> sadd myset a b c d e
(integer) 5
127.0.0.1:6379> dbsize
(integer) 5
  • exist key:判断key是否存在
127.0.0.1:6379> set a b
OK
127.0.0.1:6379> exist a
(integer) 1
127.0.0.1:6379> del a
(integer) 1
127.0.0.1:6379> exists a
(integer) 0
  • del key [key…] 删除指定key-value
127.0.0.1:6379> set a b
OK
127.0.0.1:6379> get a
"b"
127.0.0.1:6379> del a
(integer) 1
127.0.0.1:6379> get a
(nil)
  • expire key seconds 设置key过期时间
  • ttl key 查看key的剩余过期时间
  • persist key 删除key的过期设置
127.0.0.1:6379> set hello world
OK
127.0.0.1:6379> expire hello 20
(integer) 1
127.0.0.1:6379> ttl hello
(integer) 16
127.0.0.1:6379> get hello
"world"
127.0.0.1:6379> ttl hello
(integer) 7
127.0.0.1:6379> ttl hello
(integer) -2 (-2代表key已经不存在了)
127.0.0.1:6379> get hello
(nil)
127.0.0.1:6379> set hello world
OK
127.0.0.1:6379> expire hello 20
(integer) 1
127.0.0.1:6379> ttl hello
(integer) 16 (还有16秒过期)
127.0.0.1:6379> persist hello
(integer) 1
127.0.0.1:6379> ttl hello
(integer) -1 (-1代表key存在,并且没有设置过期时间)
127.0.0.1:6379> get hello
"wordl"
  • type key 查看数据类型

redis数据类型:string hash list set zset

127.0.0.1:6379> set a b
OK
127.0.0.1:6379> type a
string
127.0.0.1:6379> sadd myset 1 2 3
(integer) 3
127.0.0.1:6379> type myset
set
  • 时间复杂度

未分类

数据结构和内部编码

未分类

单线程

Redis单线程设计,因此使用时要注意几点:

  1. 一次只运行一条命令
  2. 不要执行长(慢)命令

长(慢)命令:keys, flushall, flushdb, slow lua script, mutil/exec, operate big value(collection)

字符串

使用场景

  • 缓存
  • 计数器
  • 分布式锁
  • 等等

命令

  • get、set、del
127.0.0.1:6379> set hello "world"
OK
127.0.0.1:6379> get hello
"world"
127.0.0.1:6379> del hello
1
127.0.0.1:6379> get hello
(nil) 
  • incr、decr、incrby、decrby
127.0.0.1:6379> get counter
(nil)
127.0.0.1:6379> incr counter
(integer) 1
127.0.0.1:6379> get counter
"1"
127.0.0.1:6379> incrby counter 99
(integer) 100
127.0.0.1:6379> decr counter
(integer) 99
127.0.0.1:6379> get counter
"99"
  • set、setnx、setxx、setex
127.0.0.1:6379> exists php
(integer) 0 (0代表不存在)
127.0.0.1:6379> set php good
OK
127.0.0.1:6379> sexnx php bad
(integer) 0
127.0.0.1:6379> set php best xx
OK
127.0.0.1:6379> get php
"best"
127.0.0.1:6379> exists java
(integer) 0
127.0.0.1:6379> set java best
(integer) 1
127.0.0.1:6379> set java easy xx
OK
127.0.0.1:6379> get java
"easy"
127.0.0.1:6379> exists lua
(integer) 0
127.0.0.1:6379> set lua hehe xx
(nil)
  • mset、mget
127.0.0.1:6379> mset hello world java best php good
OK
127.0.0.1:6379> mget hello java php
"world"
"best"
"good"
  • getset、append、strlen
127.0.0.1:6379> set hello world
OK
127.0.0.1:6379> getset hello php
"world"
127.0.0.1:6379> get hello
"php"
127.0.0.1:6379> append hello ",java"
(integer) 8
127.0.0.1:6379> get hello
"pho,java"
127.0.0.1:6379> strlen hello
(integer) 8
127.0.0.1:6379> set hello "足球"
OK
127.0.0.1:6379> strlen hello
(integer) 4 (一个中文占2个字节)
  • incrbyfloat、getrange、setrange
127.0.0.1:6379> incr counter
(integer) 1
127.0.0.1:6379> incrbyfloat counter 1.1
"2.1"
127.0.0.1:6379> get counter
"2.1"
127.0.0.1:6379> set hello javabest
OK
127.0.0.1:6379> getrange hello 0 2
"jav"
127.0.0.1:6379> setrange hello 4 p
(integer) 8
127.0.0.1:6379> get hello
"javapest"
  • 时间复杂度

未分类

实战

1、记录网站每个用户个人主页的访问量
incr userid:pageview(单线程:无竞争)

2、缓存视频的基本信息(数据源在MySQL中)

未分类

伪代码如下:

public VideoInfo get(long id) {
      String redisKey = redisPrefix + id;
      VideoInfo videoInfo = redis.get(redisKey);
      if (videoInfo == null) {
          videoInfo = mysql.get(id);
          if (videoInfo != null) {
              // 序列化
              redis.set(redisKey, serialize(videoInfo));
          }
      }
      return videoInfo;
  }

3、 分布式id生成器

利用了redis的单线程,即使是多个服务同时来获取ID,也得一个个来。

哈希

命令

  • hget、hset、hdel
# hset 设置field-value对
127.0.0.1:6379> hset user:1:info age 23
(integer) 1
# hget 获取field的value
127.0.0.1:6379> hget user:1:info age
"23"
127.0.0.1:6379> hset user:1:info name ronaldo
(integer) 1
# hgetall 获取所有key-value对
127.0.0.1:6379> hgetall user:1:info
1) "age"
2) "23"
3) "name"
4) "ronaldo"
# hdel 删除key-value对
127.0.0.1:6379> hdel user:1:info age
(integer) 1
127.0.0.1:6379> hgetall user:1:info
1) "name"
2) "ronaldo"
  • hexists、hlen
127.0.0.1:6379> hgetall user:1:info
1) "age"
2) "23"
3) "name"
4) "ronaldo"
# hexists 判断filed是否存在
127.0.0.1:6379> hexists user:1:info name
(integer) 1
# 判断field长度
127.0.0.1:6379> hlen user:1:info
(integer) 2
  • hmget、hmset
# hmset 设置多个filed-value对
127.0.0.1:6379> hmset user:2:info age 30 name kaka page 50
OK
127.0.0.1:6379> hlen user:2:info
(integer) 3
# hmget 获取多个field的value
127.0.0.1:6379> hmget user:2:info age name
1) "30"
2) "kaka"
  • hgetall、hvals、hkeys
# hgetall key:返回hash key对应所有的field和value
127.0.0.1:6379> hgetall user:2:info
1) "age"
2) "30"
3) "name"
4) "kaka"
5) "page"
6) "50"
# hvals key:返回hash key对应所有filed的value
127.0.0.1:6379> hvals user:2:info
1) "30"
2) "kaka"
3) "50"
# hkeys key:返回hash key对应所有field
127.0.0.1:6379> hkeys user:2:info
1) "age"
2) "name"
3) "page"
  • 对比string的api

未分类

  • 时间复杂度

未分类

  • 实战
  1. 记录网站每个用户个人主页的访问量
    hincrby user:1:info pageview count

  2. 缓存视频的基本信息(数据源在MySQL中)

伪代码如下:

public VideoInfo get(long id) {
    String redisKey = redisPrefix + id;
    Map<String, String> hashMap = redis.hgetAll(redisKey);
    VideoInfo videoInfo = transferMapToVideo(hashMap);
    if (videoInfo == null) {
        videoInfo = mysql.get(id);
        if (videoInfo != null) {
            redis.hmset(redisKey, transferVideoToMap(videoInfo));
        }
    }
    return videoInfo;
}

列表

有序可重复列表

数据结构

未分类

未分类

命令

  • rpush、lpush

未分类

未分类

  • linsert

未分类

  • lpop、rpop

未分类

未分类

  • lrem

未分类

未分类

  • ltrim

未分类

未分类

  • lrange

未分类

  • lindex

未分类

  • llen

未分类

  • lset

未分类

  • blpop、brpop

未分类

实战规则

  • 栈:LPUSH + LPOP
  • 队列: LPUSH + RPOP
  • 定长集合:LPUSH + LTRIM
  • 消息队列:LPUSH + BRPOP

集合

集合元素无序不可重复

API介绍

  • sadd srem

未分类

  • scard sismember srandmember smembers

未分类

  • sdiff sinter sunion

未分类

  • API演示
127.0.0.1:6379> sadd user:1:follow it news his sports
(integer) 4
127.0.0.1:6379> smembers user:1:follow
1) "news"
2) "his"
3) "it"
4) "sports"
127.0.0.1:6379> spop user:1:follow
"news"
127.0.0.1:6379> smembers user:1:follow
1) "his"
2) "it"
3) "sports"
127.0.0.1:6379> scard user:1:follow
(integer) 3
127.0.0.1:6379> sismember user:1:follow entertainment
(integer) 0

有序集合

有序不可重复集合

API介绍

  • zadd

未分类

  • zrem

未分类

  • zscore

未分类

  • zincrby

未分类

  • zcard

未分类

  • zrange

未分类

  • zrangebyscore

未分类

  • zcount

未分类

  • zremrangebyrank

未分类

  • zremrangebyscore

未分类

API使用演示

# zadd 添加集合元素
127.0.0.1:6379> zadd player:rank 1000 ronaldo 900 messi 800 c-ronaldo 600 kaka
(integer) 4
# zscore 获取分值
127.0.0.1:6379> zscore player:rank kaka
"600"
# zcard 获取元素个数
127.0.0.1:6379> zcard player:rank
(integer) 4
# zrank 获取排名,从0开始算
127.0.0.1:6379> zrank player:rank ronaldo
(integer) 3
# zrem 删除元素
127.0.0.1:6379> zrem player:rank messi
(integer) 1
# zrange 获取指定索引范围内的升序元素
127.0.0.1:6379> zrange player:rank 0 -1 withscores
1) "kaka"
2) "600"
3) "c-ronaldo"
4) "800"
5) "ronaldo"
6) "1000"
127.0.0.1:6379> zadd player:rank 1000 ronaldo 900 messi 800 c-ronaldo 600 kaka
(integer) 4
127.0.0.1:6379> zrange player:rank 0 -1
1) "kaka"
2) "c-ronaldo"
3) "messi"
4) "ronaldo"
127.0.0.1:6379> zcount player:rank 700 901
(integer) 2
127.0.0.1:6379> zrangebyscore player:rank 700 901
1) "c-ronaldo"
2) "messi"
127.0.0.1:6379> zremrangebyrank player:rank 0 1
(integer) 2
127.0.0.1:6379> zrange player:rank 0 -1
1) "messi"
2) "ronaldo"
127.0.0.1:6379> zrange player:rank 0 -1 withscores
1) "messi"
2) "900"
3) "ronaldo"
4) "1000"

API查漏补缺

  • zrevrank:zrank的反排
  • zrevrange zrange的反排
  • zrevrangebyscore zrangebyscore的反排
  • zinterstore 集合交集运算并存储
  • zunionstore 集合并集运算并存储

zset总结

未分类

Redis开源客户端

Redis开源客户端涵盖各种语言:

未分类

参考:https://redis.io/clients

Java客户端Jedis

这里主要详细介绍下Redis的Java开源客户端工具Jedis的使用

获取jedis

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
    <type>jar</type>
    <scope>compile</scope>
</dependency>

jedis直连

Jedis jedis = new Jedis("127.0.0.1", 6379);
jedis.set("hello", "world");
String value = jedis.get("hello");

Jedis构造函数

Jedis(String host, int port, int connectionTimeout, int soTimeout)
  • host: Redis节点机器IP
  • post: Redis端口
  • connectionTimeout: 连接超时时间
  • soTimeout: 客户端读写超时时间

这里只列了常用参数,其他像密码,或基于安全的SSL,用到的时候自行查阅即可。

简单使用

// 1. string
jedis.set("hello", world); // OK
jedis.get("hello"); // "world"
jedis.incr("counter"); // 1
// 2. hash
jedis.hset("myhash", "f1", "v1");
jedis.hset("myhash", "f2", "v2");
jedis.hgetAll("myhash"); // {f1=v1, f2=v2}
// 3. list
jedis.rpush("mylist", 1);
jedis.rpush("mylist", 2);
jedis.rpush("mylist", 3);
jedis.lrange("mylist", 0, -1); // [1, 2, 3]
// 4. set
jedis.sadd("myset", "a");
jedis.sadd("myset", "b");
jedis.sadd("myset", "a");
jedis.smembers("myset"); // [b, a]
// 5. zset
jedis.zadd("myzset", 99, "tom");
jedis.zadd("myzset", 66, "peter");
jedis.zadd("myzset", 33, "james");
jedis.zrangeWithScores("myzset", 0, -1); // [[["james"], 33.0], [["peter"], 66.0], [["tom"], 99.0]]

Jedis连接池

GenericObjectPoolConfig poolConfig = new GenericObjectPollConfig();
JedisPool jedisPool = new JedisPool(poolConfig, "127.0.0.1", 6379);
Jedis jedis = null;
try {
    // 1. 从连接池获取Jedis对象
    jedisPool.getResource();
    // 2. 执行操作
    jedis.set("hello", "world");
} catch(Exception e) {
    e.printStackTrace();
} finally {
    if (jedis != null) {
        // 3. 如果使用JedisPool,close操作不是关闭连接,而是归还连接池
        jedis.close();
    }
}

Git 版本管理流程

命名规范

  • master 分支:master;
  • develop 分支:develop;
  • release 稳定分支:release_stable_YYYYMMDD_n,YYYYMMDD 为当前日期(年月日),n 为递增序列号;
  • release 测试分支:release_nightly_YYYYMMDD_n,YYYYMMDD 为当前日期(年月日),n 为递增序列号;
  • 开发人员版分支:bug单号为 bugfree 编号,n 为递增序列号。
    • 修复 bug:fixbug_bug单号_邮箱前缀_n;
    • 需求功能:feature_bug单号_邮箱前缀_n。

基本命令

以下所有命令的开发分支号均以fixbug_12345_jingyang_1举例,合并均以合并到 develop 举例。

  • 切换分支:git checkout fixbug_12345_jingyang_1
  • 建立新分支并切换到该分支:git checkout –b fixbug_12345_jingyang_1
  • 查看所有分支:git branch -a
  • 推送分支到服务器:git push origin fixbug_12345_jingyang_1
  • 合并分支:git merge origin/fixbug_12345_jingyang_1
  • 删除分支:
    • 本地删除:git branch -d fixbug_12345_jingyang_1
    • 远程删除:git branch –r –d fixbug_12345_jingyang_1,删除后推送到服务器 git push origin :fixbug_12345_jingyang_1
  • 回退版本:git reset –hard HEAD^

基本思路

  • 开发人员分支:
    • 从release 稳定分支签出新分支;
    • 自测通过的分支会被合并到release 测试分支(合并专员不会合并有冲突的分支);
    • 最终被合并的无冲突release 测试分支会合并到 develop 分支。
  • 测试(QA)环境:测试develop 分支;
    • 测试结束后重新合并一个release 测试分支,剔除所有测试不通过的分支;
    • 版本会合并到release 稳定分支。
  • 生产(Prod)环境:进行冒烟测试;
    • 测试结束后重新合并一个release 稳定分支,剔除所有测试不通过的分支;
    • 通过的release 稳定分支会同步到局方环境;
  • 局方 CUC 环境:运行最终的 release 稳定分支。

未分类

注意事项

  • 合并release 测试分支过程中不进行冲突合并,遇到冲突即回退到上次提交;
  • 开发人员未被合并的分支,将在下一次另建分支合并;
  • release 稳定版应保留多个历史版本;
  • 应在版本被推送到局方环境之后再考虑删除开发人员分支。

场景

以下场景均假设有最近稳定版本release_stable_20171221_1,修改 bug 编号12345.

1、新建分支修改 bug

  • 拉取代码,确保获得最新版本:git pull;
  • 签出release 稳定分支分支:git checkout release_stable_20171221_1;
  • 在稳定分支基础上,新建自己的分支:git checkout -b fixbug_12345_jingyang_1;
  • 在自己的分支上修改代码,并提交。

2、合并分支冲突

合并过程中遇到冲突的分支将不会被合并。由于项目文件较多,冲突并不常见。如果遇有冲突,开发人员应该自己解决。

以下设想了两种场景,但无论是哪种场景,开发人员都应该找到冲突原因,协商解决。

  • 如果今天还有合并分支的机会:找到冲突原因,协商解决,确保下次合并没有冲突;如果已经发布了新的稳定版本,合并稳定版本到自己的版本
  • 如果赶不上今天的合并:签出最新release 稳定分支,并合并到自己的分支中,确保代码是最新的。
  • 合并release 稳定分支到自己的分支:git merge release_stable_20171221_1 fixbug_12345_jingyang_1

3、测试人员测试不通过

  • 放弃之前的分支fixbug_12345_jingyang_1;
  • 从最新的release 稳定分支签出新的分支:git checkout -b fixbug_12345_jingyang_2;
  • 把前一个分支合并到新分支 git merge fixbug_12345_jingyang_1 fixbug_12345_jingyang_2,并重新开始修改。

4、局方测试不通过

局方不通过时,通常代码已经被合并到最新release 稳定分支中。

  • 从最新release 稳定分支签出新分支:git checkout -b fixbug_12345_jingyang_2;
  • 在新分支上进行修改。

5、bug修改经历多日

记得在最终合并分支之前把最新的release 稳定分支合并到自己的分支。

PHP实现git部署

背景

小公司上班,原先的系统还在使用传统的ftp上传部署,部署太麻烦,也不好管控线上的代码.在网上找了下git部署的教程.一路跟着下来,碰到了不少坑.现在把整个过程发出来,希望可以帮助到大家.

账号相关

useradd -m git    //新增git账号

ssh-keygen        //密钥生成,如果已经有了可略过

su git       //切换至git账号

cd ~   //切换到git账号根目录

mkdir .ssh    //创建.ssh目录

cat /密钥存放目录/xxx.pub >> ~/.ssh/authorized_keys     //设置公钥

此处一定要注意权限问题,否则密钥登入无法生效,各文件夹对应的权限如下

.ssh文件夹权限   700
id_rsa   600
id_rsa.pub   644
authorized_keys  600

文件权限设置

将git与站点运行nginx或者apache的用户放同一用户组,如www
vim /etc/passwd 找到git账号与www账号,将git账号的组标识变更为与www组标识一致
站点所属者设置为git,用户组设置为 www //假设nginx与git 都归属于www用户组
目录权限设置775 ,文件权限设置为664

仓库代码

服务器端

cd 站点目录
git init   //初始化目录

git配置

git config receive.denyCurrentBranch ignore     //设置仓库接受代码提交

设置sudo免密

vim /etc/sudoers

# Defaults        secure_path 中若没有你要的命令,要注意添加
# php的命令默认需要在 secure_path最后面添加    :/usr/local/php/bin"
在  root ALL=(ALL) ALL 下面一行添加
git ALL=(ALL) NOPASSWD:ALL     
# 保存退出   ,这样针对laravel 要重启队列命令就可以使用了.
# sudo php artisan queue:restart

钩子设置

cd .git/hooks    //切换至站点钩子目录
touch post-receive    //创建接收提交时钩子
// 钩子文件内容如下:

#!/bin/sh
# 设置账号创建文件的默认权限
umask 002   
unset GIT_DIR
cd ..
git checkout -f
# 执行PHP钩子逻辑
/usr/bin/curl http(s)://域名/钩子文字位置/hook.php

# 如果有使用laravel队列则需要重启队列进程,让新代码生效
# sudo php artisan queue:restart
exit 0

hook.php内容

<?php
/**
 * git上传执行钩子
 */
//TODO 安全限制
//TODO 其他钩子行为
// 清除opcache
if (version_compare(PHP_VERSION, '5.5.0', '>=')) {
    opcache_reset();
}

增加钩子可执行权限

chmod a+x .git/hooks/post-receive

本地代码

git remote add 远程仓库名称 ssh://git@IP地址:/站点目录    //添加远程仓库
git push 远程仓库名称  master 

特别注意

用户上传的图片目录一定要做好文件忽视动作,否则有可能在清除未追踪文件时将此部分文件删除,造成灾难性结果。