一、概述

Redis 是速度非常快的非关系型(NoSQL)内存键值数据库,可以存储键和五种不同类型的值之间的映射。

键的类型只能为字符串,值支持五种数据类型:字符串、列表、集合、散列表、有序集合。

Redis 支持很多特性,例如将内存中的数据持久化到硬盘中,使用复制来扩展读性能,使用分片来扩展写性能。

二、数据类型

数据类型 可以存储的值 操作
STRING 字符串、整数或者浮点数 对整个字符串或者字符串的其中一部分执行操作 对整数和浮点数执行自增或者自减操作
LIST 列表 从两端压入或者弹出元素 对单个或者多个元素 进行修剪,只保留一个范围内的元素
SET 无序集合 添加、获取、移除单个元素 检查一个元素是否存在于集合中 计算交集、并集、差集 从集合里面随机获取元素
HASH 包含键值对的无序散列表 添加、获取、移除单个键值对 获取所有键值对 检查某个键是否存在
ZSET 有序集合 添加、获取、删除元素 根据分值范围或者成员来获取元素 计算一个键的排名

string

string 是 redis 最基本的类型,你可以理解成与 Memcached 一模一样的类型,一个 key 对应一个 value。

string 类型是二进制安全的。意思是 redis 的 string 可以包含任何数据。比如 jpg 图片或者序列化的对象 。

string 类型是 Redis 最基本的数据类型,一个 redis 中字符串 value 最多可以是 512M。

list

Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素导列表的头部(左边)或者尾部(右边)。它的底层实际是个链表。

set

Redis 的 set 是 string 类型的无序集合。它是通过 HashTable 实现实现的。

hash

Redis hash 是一个键值对集合。

Redis hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象。

类似 Java 里面的 Map<String,Object>。

zset

sorted set:有序集合。

Redis zset 和 set 一样也是 string 类型元素的集合,且不允许重复的成员。

不同的是每个元素都会关联一个 double 类型的分数。

redis 正是通过分数来为集合中的成员进行从小到大的排序。zset 的成员是唯一的,但分数 (score) 却可以重复。

Redis常见数据结构操作命令

http://redisdoc.com/

三、常用基本操作

key

指令 作用
keys * 查看所有 key
exists key的名字 判断某个 key 是否存在
move key db 移动 key 到 db
expire key 秒数 为 key 设置过期时间
ttl key 查看还有所少秒过期,-1表示永不过期,-2表示已过期
type key 查看 key 的类型

string (单值单 value)

指令 作用
set/get/del/append/strlen 增、查、删、追加、返回长度
lncr/decr/incrby/decrby 加1,减1,加n,减n(数字才能加减,n路:incrby k1 n)
getrange/setrange getrange: 获取指定区间范围内的值,从零到负一表示全部。
setrange: 设置指定区间范围内的值,格式是setrange key 开始位 值(例:setrange k1 0 xxx)
setex key 秒数 值 即 set with expire,设置带过期时间的 key,动态设置
setnx key value 即 set if not exist,只有在 key 不存在时设置 key 的值
mset/mget/msetnx mset: 同时设置一个或多个 key-value 对。
mget: 获取所有(一个或多个)给定 key 的值。
msetnx: 同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在。
getset 即先 get 再 set,将给定 key 的值设为 value ,并返回 key 的旧值(old value)。

list (单值多 value)

指令 作用
lpush/rpush/lrange lpush: 从左侧按顺序添加(例: lpush list01 1 2 3 4 5)
rpush: 从右侧按顺序添加(例: rpush list02 1 2 3 4 5)
lrange: 按范围查询(lrange 列表名 0 -1 查询全部)
lpop/rpop lpop: 左出;rpop: 右出(lpop/rpop 列表名)
lindex 通过索引获取列表中的元素 lindex key index
llen 返回列表 key 的长度
lerm key count value 删除 count 个等于 value 的 key,如果 count 为0则删除全部
ltrim key startindex endindex 截取指定索引区间的元素
rpoplpush 源列表 目的列表 移除列表的最后一个元素,并将该元素添加到另一个列表并返回
lset key index value 通过索引设置列表元素的值
linsert key before/after value1 value2 在 list 某个已有值的前后再添加具体值

性能分析

它是一个字符串链表,left、right 都可以插入添加;
如果键不存在,创建新的链表;
如果键已存在,新增内容;
如果值全移除,对应的键也就消失了。
链表的操作无论是头和尾效率都极高,但假如是对中间元素进行操作,效率就很惨淡了。

set (单值多 value)

参数 作用
sadd/smembers/sismember 添加、返回集合中的所有的成员(smembers key)、判断成员元素是否是集合的成员(sismember key value)
scard key 返回集合中元素的数量
srem key value1…valueN 移除集合中的一个或多个成员元素,不存在的成员元素会被忽略。
srandmember key [count] 返回集合中的一个随机元素。如果 count 为正数,且小于集合基数,那么命令返回一个包含 count 个元素的数组,数组中的元素各不相同。如果 count 大于等于集合基数,那么返回整个集合。如果 count 为负数,那么命令返回一个数组,数组中的元素可能会重复出现多次,而数组的长度为 count 的绝对值。
spop key [count] 用于移除集合中的指定 key 的一个或多个随机元素,移除后会返回移除的元素。
smove key1 key2 在key1里某个值 作用是将 key1 里的某个值赋给 key2
sdiff FIRST_KEY OTHER_KEY1..OTHER_KEYN 返回给定集合之间的差集。差集的结果来自前面的 FIRST_KEY ,而不是后面的 OTHER_KEY1,也不是整个 FIRST_KEY OTHER_KEY1..OTHER_KEYN 的差集。
sunion KEY KEY1..KEYN 返回给定集合的并集。不存在的集合 key 被视为空集。
sinter KEY KEY1..KEYN 返回给定所有给定集合的交集。 不存在的集合 key 被视为空集。 当给定集合当中有一个空集时,结果也为空集(根据集合运算定律)。

hash (KV 模式不变,但 V 是一个键值对)

参数 作用
hset/hget/hmset/hmget/hgetall/hdel 设值、获取、同时设值多个、获取所有给定字段的值、获取在哈希表中指定 key 的所有字段和值、删除一个或多个哈希表字段
hlen key 获取哈希表中字段的数量
hexists key field 查看哈希表 key 中,指定的字段是否存在
hkeys key/hvals key 获取所有哈希表中的字段、获取哈希表中所有值
hincrby key field increment/hincrbyfloatkey field increment 为哈希表 key 中的指定字段的整数值加上增量 increment 、为哈希表 key 中的指定字段的浮点数值加上增量 increment 。
hsetnx key field value 只有在字段 field 不存在时,设置哈希表字段的值。

zset (sorted set,在set基础上,加一个score值。之前set是k1 v1 v2 v3,现在zset是k1 score1 v1 score2 v2)

参数 作用
zadd/zrange 向有序集合添加一个或多个成员,或者更新已存在成员的分数、通过索引区间返回有序集合成指定区间内的成员
zrangebyscore key min max [WITHSCORES] [LIMIT] 返回有序集合中指定分数区间的成员列表。
zrem key member [member …] 移除有序集中的一个或多个成员,不存在的成员将被忽略。
zcard key/zcount key min max/zrank key member/zscore key member 获取有序集合的成员数、计算在有序集合中指定区间分数的成员数、返回有序集合中指定成员的索引、返回有序集中,成员的分数值
zrevrank key member 返回有序集合中指定成员的排名,有序集成员按分数值递减(从大到小)排序
zrevrange key start stop [WITHSCORES] 返回有序集中指定区间内的成员,通过索引,分数从高到底
zrevrangebyscore key max min [WITHSCORES] 返回有序集中指定分数区间内的成员,分数从高到低排序

四、配置文件

  1. Redis默认不是以守护进程的方式运行,可以通过该配置项修改,使用yes启用守护进程
    daemonize no
  2. 当Redis以守护进程方式运行时,Redis默认会把pid写入/var/run/redis.pid文件,可以通过pidfile指定
    pidfile /var/run/redis.pid
  3. 指定Redis监听端口,默认端口为6379,作者在自己的一篇博文中解释了为什么选用6379作为默认端口,因为6379在手机按键上MERZ对应的号码,而MERZ取自意大利歌女Alessia Merz的名字
    port 6379
  4. 绑定的主机地址
    bind 127.0.0.1
  5. 当客户端闲置多长时间后关闭连接,如果指定为0,表示关闭该功能
    timeout 300
  6. 指定日志记录级别,Redis总共支持四个级别:debug、verbose、notice、warning,默认为verbose
    loglevel verbose
  7. 日志记录方式,默认为标准输出,如果配置Redis为守护进程方式运行,而这里又配置为日志记录方式为标准输出,则日志将会发送给/dev/null
    logfile stdout
  8. 设置数据库的数量,默认数据库为0,可以使用SELECT 命令在连接上指定数据库id
    databases 16
  9. 指定在多长时间内,有多少次更新操作,就将数据同步到数据文件,可以多个条件配合
    save
    Redis默认配置文件中提供了三个条件:
    save 900 1
    save 300 10
    save 60 10000
    分别表示900秒(15分钟)内有1个更改,300秒(5分钟)内有10个更改以及60秒内有10000个更改。
  10. 指定存储至本地数据库时是否压缩数据,默认为yes,Redis采用LZF压缩,如果为了节省CPU时间,可以关闭该选项,但会导致数据库文件变的巨大
    rdbcompression yes
  11. 指定本地数据库文件名,默认值为dump.rdb
    dbfilename dump.rdb
  12. 指定本地数据库存放目录
    dir ./
  13. 设置当本机为slav服务时,设置master服务的IP地址及端口,在Redis启动时,它会自动从master进行数据同步
    slaveof
  14. 当master服务设置了密码保护时,slav服务连接master的密码
    masterauth
  15. 设置Redis连接密码,如果配置了连接密码,客户端在连接Redis时需要通过AUTH 命令提供密码,默认关闭
    requirepass foobared
  16. 设置同一时间最大客户端连接数,默认无限制,Redis可以同时打开的客户端连接数为Redis进程可以打开的最大文件描述符数,如果设置 maxclients 0,表示不作限制。当客户端连接数到达限制时,Redis会关闭新的连接并向客户端返回max number of clients reached错误信息
    maxclients 128
  17. 指定Redis最大内存限制,Redis在启动时会把数据加载到内存中,达到最大内存后,Redis会先尝试清除已到期或即将到期的Key,当此方法处理 后,仍然到达最大内存设置,将无法再进行写入操作,但仍然可以进行读取操作。Redis新的vm机制,会把Key存放内存,Value会存放在swap区
    maxmemory
  18. 指定是否在每次更新操作后进行日志记录,Redis在默认情况下是异步的把数据写入磁盘,如果不开启,可能会在断电时导致一段时间内的数据丢失。因为 redis本身同步数据文件是按上面save条件来同步的,所以有的数据会在一段时间内只存在于内存中。默认为no
    appendonly no
  19. 指定更新日志文件名,默认为appendonly.aof
    appendfilename appendonly.aof
  20. 指定更新日志条件,共有3个可选值:
    no:表示等操作系统进行数据缓存同步到磁盘(快)
    always:表示每次更新操作后手动调用fsync()将数据写到磁盘(慢,安全)
    everysec:表示每秒同步一次(折衷,默认值)
    appendfsync everysec
  21. 指定是否启用虚拟内存机制,默认值为no,简单的介绍一下,VM机制将数据分页存放,由Redis将访问量较少的页即冷数据swap到磁盘上,访问多的页面由磁盘自动换出到内存中(在后面的文章我会仔细分析Redis的VM机制)
    vm-enabled no
  22. 虚拟内存文件路径,默认值为/tmp/redis.swap,不可多个Redis实例共享
    vm-swap-file /tmp/redis.swap
  23. 将所有大于vm-max-memory的数据存入虚拟内存,无论vm-max-memory设置多小,所有索引数据都是内存存储的(Redis的索引数据 就是keys),也就是说,当vm-max-memory设置为0的时候,其实是所有value都存在于磁盘。默认值为0
    vm-max-memory 0
  24. Redis swap文件分成了很多的page,一个对象可以保存在多个page上面,但一个page上不能被多个对象共享,vm-page-size是要根据存储的 数据大小来设定的,作者建议如果存储很多小对象,page大小最好设置为32或者64bytes;如果存储很大大对象,则可以使用更大的page,如果不 确定,就使用默认值
    vm-page-size 32
  25. 设置swap文件中的page数量,由于页表(一种表示页面空闲或使用的bitmap)是在放在内存中的,,在磁盘上每8个pages将消耗1byte的内存。
    vm-pages 134217728
  26. 设置访问swap文件的线程数,最好不要超过机器的核数,如果设置为0,那么所有对swap文件的操作都是串行的,可能会造成比较长时间的延迟。默认值为4
    vm-max-threads 4
  27. 设置在向客户端应答时,是否把较小的包合并为一个包发送,默认为开启
    glueoutputbuf yes
  28. 指定在超过一定的数量或者最大的元素超过某一临界值时,采用一种特殊的哈希算法
    hash-max-zipmap-entries 64
    hash-max-zipmap-value 512
  29. 指定是否激活重置哈希,默认为开启(后面在介绍Redis的哈希算法时具体介绍)
    activerehashing yes
  30. 指定包含其它的配置文件,可以在同一主机上多个Redis实例之间使用同一份配置文件,而同时各个实例又拥有自己的特定配置文件
    include /path/to/local.conf

五、持久化

Redis 是内存型数据库,为了保证数据在断电后不会丢失,需要将内存中的数据持久化到硬盘上。

RDB (Redis DataBase)

是什么

在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是行话讲的 Snapshot 快照,它恢复时是将快照文件直接读到内存里。(rdb 保存的是 dump.rdb 文件)

Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。

整个过程中,主进程是不进行任何 IO 操作的,这就确保了极高的性能。

如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那 RDB 方式要比AOF 方式更加的高效。RDB 的缺点是最后一次持久化后的数据可能丢失。

fork

fork 的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、程序计数器等)数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程。

如何触发 RDB 快照

  1. 配置文件中默认的快照配置,1分钟改1w次或5分钟改10次或15分钟改1次(可以冷拷贝后重新使用cp dump.rdb dump_new.rdb)

  2. 命令 save 或者 bgsave

    Save:save 时只管保存,其它不管,全部阻塞。

    BGSAVE:Redis 会在后台异步进行快照操作,快照同时还可以响应客户端请求。可以通过 lastsave 命令获取最后一次成功执行快照的时间

  3. 执行 flushall 命令,也会产生 dump.rdb 文件,但里面是空的,无意义

如何恢复

将备份文件 (dump.rdb) 移动到 redis 安装目录并启动服务即可

CONFIG GET dir 获取目录

优劣

优势:适合大规模的数据恢复,对数据完整性和一致性要求不高

劣势:在一定间隔时间做一次备份,所以如果 redis 意外 down 掉的话,就

会丢失最后一次快照后的所有修改;fork 的时候,内存中的数据被克隆了一份,大致2倍的膨胀性需要考虑

如何停止

动态停止所有 RDB 保存规则的方法:redis-cli config set save “”

AOF (Append Only File)

是什么

以日志的形式来记录每个写操作,将 redis 执行过的所有写指令记录下来(读操作不记录),只许追加文件但不可以改写文件,redis 启动之初会读取该文件重新构建数据,换言之,redis 重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。(AOF 保存的是 appendonly.aof 文件)

AOF 启动/修复/恢复

正常恢复

  1. 启动:修改默认的 appendonly no,改为 yes
  2. 将有数据的 AOF 文件复制一份保存到对应目录( config get dir )
  3. 恢复:重启 redis 然后重新加载

异常恢复

  1. 启动:修改默认的 appendonly no,改为 yes
  2. 备份被写坏的 AOF 文件
  3. 修复:redis-check-aof –fix 进行修复
  4. 恢复:重启 redis 然后重新加载

rewrite

是什么

AOF 采用文件追加方式,文件会越来越大为避免出现此种情况,新增了重写机制,当 AOF 文件的大小超过所设定的阈值时,Redis 就会启动 AOF 文件的内容压缩,只保留可以恢复数据的最小指令集。可以使用命令 bgrewriteaof。

重写原理

AOF 文件持续增长而过大时,会 fork 出一条新进程来将文件重写(也是先写临时文件最后再 rename),遍历新进程的内存中数据,每条记录有一条的 Set 语句。重写 AOF 文件的操作,并没有读取旧的 AOF 文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的 AOF 文件,这点和快照有点类似。

触发机制

redis 会记录上次重写时的 AOF 大小,默认配置是当 AOF 文件大小是上次 rewrite 后大小的一倍且文件大于 64M 时触发。

优劣

优势:每修改同步:appendfsync always 同步持久化 每次发生数据变更会被立即记录到磁盘 性能较差但数据完整性比较好;每秒同步:appendfsync everysec 异步操作,每秒记录 如果一秒内宕机,有数据丢失;不同步:appendfsync no 从不同步。

劣势:相同数据集的数据而言 aof 文件要远大于 rdb 文件,恢复速度慢于 rdb;aof 运行效率要慢于 rdb,每秒同步策略效率较好,不同步效率和 rdb 相同。

总结(用哪个)

  1. RDB 持久化方式能够在指定的时间间隔能对你的数据进行快照存储
  2. AOF 持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF 命令以 redis 协议追加保存每次写的操作到文件末尾。redis 还能对 AOF 文件进行后台重写,使得 AOF 文件的体积不至于过大。
  3. 只做缓存:如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化方式。
  4. 同时开启两种持久化方式
    • 在这种情况下,当 redis 重启的时候会优先载入 AOF 文件来恢复原始的数据,因为在通常情况下 AOF 文件保存的数据集要比 RDB 文件保存的数据集要完整。
    • RDB 的数据不实时,同时使用两者时服务器重启也只会找 AOF 文件。那要不要只使用 AOF 呢?作者建议不要,因为 RDB 更适合用于备份数据库(AOF 在不断变化不好备份),快速重启,而且不会有 AOF 可能潜在的 bug,留着作为一个万一的手段。
  5. 性能建议
    • 因为 RDB 文件只用作后备用途,建议只在 Slave 上持久化 RDB 文件,而且只要15分钟备份一次就够了,只保留 save 900 1 这条规则。
    • 如果 Enalbe AOF,好处是在最恶劣情况下也只会丢失不超过两秒数据,启动脚本较简单只 load 自己的 AOF 文件就可以了。代价一是带来了持续的 IO,二是 AOF rewrite 的最后将 rewrite 过程中产生的新数据写到新文件造成的阻塞几乎是不可避免的。只要硬盘许可,应该尽量减少 AOF rewrite 的频率,AOF 重写的基础大小默认值64M 太小了,可以设到 5G 以上。默认超过原大小 100% 大小时重写可以改到适当的数值。
    • 如果不 Enable AOF ,仅靠 Master-Slave Replication 实现高可用性也可以。能省掉一大笔 IO 也减少了 rewrite 时带来的系统波动。代价是如果 Master/Slave 同时倒掉,会丢失十几分钟的数据,启动脚本也要比较两个 Master/Slave 中的 RDB 文件,载入较新的那个。新浪微博就选用了这种架构。

六、事务

是什么

可以一次执行多个命令,本质是一组命令的集合。一个事务中的所有命令都会序列化,按顺序地串行化执行而不会被其它命令插入,不许加塞。

能干嘛

一个队列中,一次性、顺序性、排他性的执行一系列命令。

场景

  1. 正常执行
  2. 放弃事务
  3. 全体连坐(一条有错,全军覆没)
  4. 冤头债主(exec 才发现的错误,不影响事务中其他语句的执行)
  5. watch 监控
    • 场景:初始化信用卡可用余额和欠额
      • 无加塞篡改,先监控再开启 multi,保证两笔金额变动在同一个事务内。
      • 有加塞篡改,监控了 key,如果 key 被修改了,后面一个事务的执行失效。
      • unwatch
      • 一旦执行了 exec 之前加的监控锁都会被取消掉了
    • 小结
      • Watch 指令,类似乐观锁,事务提交时,如果 Key 的值已被别的客户端改变,比如某个 list 已被别的客户端 push/pop 过了,整个事务队列都不会被执行。
      • 通过 WATCH 命令在事务执行之前监控了多个 Keys,倘若在 WATCH 之后有任何 Key 的值发生了变化,EXEC 命令执行的事务都将被放弃,同时返回 Nullmulti-bulk 应答以通知调用者事务执行失败。

指令

指令 作用
DISCARD 取消事务,放弃执行事务块内的所有命令。
EXEC 执行所有事务块内的命令。
MULTI 标记一个事务块的开始。
UNWATCH 取消 WATCH 命令对所有 key 的监视。
WATCH key [key …] 监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。

三阶段

开启:以 MULTI 开始一个事务。

入队:将多个命令入队到事务中,接到这些命令并不会立即执行,而是放到等待执行的事务队列里面。

执行:由 EXEC 命令触发事务。

三特性

单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。

没有隔离级别的概念:队列中的命令没有提交之前都不会实际的被执行,因为事务提交前任何指令都不会被实际执行,也就不存在”事务内的查询要看到事务里的更新,在事务外查询不能看到”这个让人万分头痛的问题。

不保证原子性:redis 同一个事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚。(部分支持事务:全体连坐和冤头债主)

七、发布订阅

是什么

进程间的一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。

下图展示了频道 channel1 , 以及订阅这个频道的三个客户端 —— client2 、 client5 和 client1 之间的关系:

当有新消息通过 PUBLISH 命令发送给频道 channel1 时, 这个消息就会被发送给订阅它的三个客户端:

实际中不会用 redis 做消息中间件

八、复制

是什么

行话:也就是我们所说的主从复制,主机数据更新后根据配置和策略,自动同步到备机的 master/slaver 机制,Master以写为主,Slave以读为主。

能干嘛

读写分离、容灾恢复

怎么玩

  1. 配从(库)不配主(库)

  2. 从库配置:slaveof 主库IP 主库端口

    • 每次与 master 断开之后,都需要重新连接,除非你配置进 redis.conf 文件
    • info replication
  3. 修改配置文件细节操作

    • 拷贝多个 redis.conf 文件
    • 开启 daemonize yes
    • pid 文件名字 (pidfile)
    • 指定端口 (port)
    • log 文件名字 (logfile)
    • dump.rdb 名字 (dbfilename)
  4. 常用技能

    • 一主二仆

      • Init

      • 一个 Master 两个 Slave

      • 日志查看:主机日志、备机日志、info replication(查看状态)

      • 一些问题

        1 切入点问题?slave1、slave2 是从头开始复制还是从切入点开始复制?比如从 k4 进来,那之前的123是否也可以复制?(✔️)

        2 从机是否可以写?set可否?(❌,读写分离呀,从机不可写。)

        3 主机 shutdown 后情况如何?从机是上位还是原地待命?(从机原地待命。)

        4 主机又回来了后,主机新增记录,从机还能否顺利复制?(可以,主机回来自动恢复)

        5 其中一台从机 down 后情况如何?依照原有它能跟上大部队吗?(从机只要和 master 断开就要重连)

    • 薪火相传

      • 上一个 Slave 可以是下一个 Slave 的 Master,Slave 同样可以接收其他 Slaves 的连接和同步请求,那么该 Slave 作为了链条中下一个的 Master,可以有效减轻 Master 的写压力。
      • 中途变更转向:会清除之前的数据,重新建立拷贝最新的。
      • slaveof 新主库IP 新主库端口
    • 反客为主

      • SLAVEOF no one
      • 使当前数据库停止与其他数据库的同步,转成主数据库

复制原理

  1. slave 启动成功连接到 master 后会发送一个 sync 命令。
  2. Master 接到命令启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行完毕之后,master 将传送整个数据文件到 slave,以完成一次完全同步。
  3. 全量复制:而 slave 服务在接收到数据库文件数据后,将其存盘并加载到内存中。
  4. 增量复制:Master 继续将新的所有收集到的修改命令依次传给 slave,完成同步。
  5. 但是只要是重新连接 master,一次完全同步(全量复制)将被自动执行。

哨兵模式 (sentinel)

反客为主的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库。一组 sentinel 能同时监控多个 Master。

使用步骤

  1. 调整结构,6379带着80、81
  2. 自定义的 /myredis 目录下新建 sentinel.conf 文件,名字绝不能错
  3. 配置哨兵,填写内容
    • sentinel monitor 被监控数据库名字(自己起名字) 127.0.0.1 6379 1
    • 上面最后一个数字1,表示主机挂掉后 salve 投票看让谁接替成为主机,得票数多少后成为主机
  4. 启动哨兵
    • redis-sentinel /myredis/sentinel.conf
    • 上述目录依照各自的实际情况配置,可能目录不同
  5. 正常主从演示,原有的master挂了,投票新选
  6. 重新主从继续开工,info replication查查看
  7. 问题:如果之前的 master 重启回来,会不会双 master 冲突?(❌,和反客为主不大一样,老 master 回来会变成新 master 的 slave)

复制的缺点

复制延时:由于所有的写操作都是先在 Master 上操作,然后同步更新到 Slave 上,所以从 Master 同步到 Slave 机器有一定的延迟,当系统很繁忙的时候,延迟问题会更加严重,Slave 机器数量的增加也会使这个问题更加严重。

九、Jedis

package

commons-pool-1.6.jar、jedis-2.1.0.jar

连通性测试

返回 pong 则为连通

1
2
3
4
5
6
7
8
9
10
import redis.clients.jedis.Jedis;

public class Main {
public static void main(String[] args) {
//连接本地的 Redis 服务
Jedis jedis = new Jedis("127.0.0.1",6379);
// 查看服务是否运行,打出pong表示OK
System.out.println("connection is OK==========>: " + jedis.ping());
}
}

日常使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.util.Set;
import redis.clients.jedis.Jedis;

public class TestAPI {
public static void main(String[] args)
{
Jedis jedis = new Jedis("127.0.0.1",6379);

jedis.set("k1","v1");
jedis.set("k2","v2");
jedis.set("k3","v3");


System.out.println(jedis.get("k3"));

Set<String> sets = jedis.keys("*");
System.out.println(sets.size());

//后续请参考脑图,家庭作业,敲一遍......
}
}

事务

加锁事务场景模拟

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;

public class TestTX {
public boolean transMethod() throws InterruptedException {
Jedis jedis = new Jedis("127.0.0.1", 6379);
int balance;// 可用余额100元(set balance 100)
int debt;// 欠额(set debt 0)
int amtToSubtract = 10;// 实刷额度10元

jedis.watch("balance");
// jedis.set("balance","5");
// 此句不该出现,模拟其他程序已经修改了该条目
// 模拟系统停顿7秒,期间若有改动,则该程序能够监控到变化
Thread.sleep(7000);
balance = Integer.parseInt(jedis.get("balance"));
if (balance < amtToSubtract) {
jedis.unwatch();
System.out.println("modify");
return false;
} else {
System.out.println("***********transaction");
Transaction transaction = jedis.multi();
transaction.decrBy("balance", amtToSubtract);
transaction.incrBy("debt", amtToSubtract);
transaction.exec();
balance = Integer.parseInt(jedis.get("balance"));
debt = Integer.parseInt(jedis.get("debt"));

System.out.println("*******" + balance);
System.out.println("*******" + debt);
return true;
}
}

/**
* 通俗点讲,watch命令就是标记一个键,如果标记了一个键,
* 在提交事务前如果该键被别人修改过,那事务就会失败,这种情况通常可以在程序中
* 重新再尝试一次。
* 首先标记了键balance,然后检查余额是否足够,不足就取消标记,并不做扣减;
* 足够的话,就启动事务进行更新操作,
* 如果在此期间键balance被其它人修改, 那在提交事务(执行exec)时就会报错,
* 程序中通常可以捕获这类错误再重新执行一次,直到成功。
*
* @throws InterruptedException
*/
public static void main(String[] args) throws InterruptedException {
TestTX test = new TestTX();
boolean retValue = test.transMethod();
System.out.println("main retValue-------: " + retValue);
}
}

主从复制

6379,6380启动,先各自先独立,主写从读。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import redis.clients.jedis.Jedis;

public class TestMS {
public static void main(String[] args) {
Jedis jedis_M = new Jedis("127.0.0.1",6379);
Jedis jedis_S = new Jedis("127.0.0.1",6380);

jedis_S.slaveof("127.0.0.1",6379);

jedis_M.set("class","1122V2");

String result = jedis_S.get("class");
System.out.println(result);
}
}

JedisPool

获取 Jedis 实例需要从 JedisPool 中获取。

用完 Jedis 实例需要返还给 JedisPool。

如果 Jedis 在使用过程中出错,则也需要还给 JedisPool。

连接池(懒汉式单例):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

public class JedisPoolUtil
{
private static volatile JedisPool jedisPool = null;

private JedisPoolUtil(){}

public static JedisPool getJedisPoolInstance()
{
if(null == jedisPool)
{
synchronized (JedisPoolUtil.class)
{
if(null == jedisPool)
{
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxActive(1000);
poolConfig.setMaxIdle(32);
poolConfig.setMaxWait(100*1000);
poolConfig.setTestOnBorrow(true);

jedisPool = new JedisPool(poolConfig,"127.0.0.1",6379);
}
}
}
return jedisPool;
}

public static void release(JedisPool jedisPool,Jedis jedis)
{
if(null != jedis)
{
jedisPool.returnResourceObject(jedis);
}
}
}

测试连接池:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

public class TestPool {

public static void main(String[] args) {
JedisPool jedisPool = JedisPoolUtil.getJedisPoolInstance();
JedisPool jedisPool2 = JedisPoolUtil.getJedisPoolInstance();

System.out.println(jedisPool == jedisPool2);

Jedis jedis = null;
try {
jedis = jedisPool.getResource();
jedis.set("aa","bb");
} catch (Exception e) {
e.printStackTrace();
}finally{
JedisPoolUtil.release(jedisPool, jedis);
}
}
}

配置

JedisPool 的配置参数大部分是由 JedisPoolConfig 的对应项来赋值的。

maxActive:控制一个 pool 可分配多少个 jedis 实例,通过 pool.getResource() 来获取;如果赋值为 -1,则表示不限制;如果 pool 已经分配了 maxActive 个 jedis 实例,则此时 pool 的状态为 exhausted。

maxIdle:控制一个 pool 最多有多少个状态为 idle(空闲) 的 jedis 实例。

whenExhaustedAction:表示当 pool 中的 jedis 实例都被 allocated 完时,pool 要采取的操作,默认有三种:

  • WHEN_EXHAUSTED_FAIL –> 表示无jedis实例时,直接抛出 NoSuchElementException
  • WHEN_EXHAUSTED_BLOCK –> 则表示阻塞住,或者达到 maxWait 时抛出JedisConnectionException
  • WHEN_EXHAUSTED_GROW –> 则表示新建一个jedis实例,也就说设置的maxActive无用;

maxWait:表示当 borrow 一个 jedis 实例时,最大的等待时间,如果超过等待时间,则直接抛 JedisConnectionException。

testOnBorrow:获得一个 jedis 实例的时候是否检查连接可用性(ping());如果为 true,则得到的 jedis 实例均是可用的。

testOnReturn:return 一个 jedis 实例给 pool 时,是否检查连接可用性(ping());

testWhileIdle:如果为 true,表示有一个 idle object evitor 线程对 idle object 进行扫描,如果 validate 失败,此 object 会被从 pool 中 drop 掉;这一项只有在 timeBetweenEvictionRunsMillis 大于0时才有意义。

timeBetweenEvictionRunsMillis:表示 idle object evitor 两次扫描之间要 sleep 的毫秒数。

numTestsPerEvictionRun:表示 idle object evitor 每次扫描的最多的对象数。

minEvictableIdleTimeMillis:表示一个对象至少停留在 idle 状态的最短时间,然后才能被 idle object evitor 扫描并驱逐;这一项只有在 timeBetweenEvictionRunsMillis 大于0时才有意义。

softMinEvictableIdleTimeMillis:在 minEvictableIdleTimeMillis 基础上,加入了至少 minIdle 个对象已经在 pool 里面了。如果为 -1,evicted 不会根据 idle time 驱逐任何对象。如果 minEvictableIdleTimeMillis>0,则此项设置无意义,且只有在 timeBetweenEvictionRunsMillis 大于0时才有意义;

lifo:borrowObject 返回对象时,是采用 DEFAULT_LIFO(last in first out,即类似cache的最频繁使用队列),如果为 False,则表示 FIFO 队列;


其中 JedisPoolConfig 对一些参数的默认设置如下:

1
2
3
4
testWhileIdle = true
minEvictableIdleTimeMills = 60000
timeBetweenEvictionRunsMillis = 30000
numTestsPerEvictionRun = -1