缓存数据库Redis

NoSQL 入门与概述

为什么用 NoSQL?

什么是单机 MySQL?

在90年代,一个网站的访问量一般都不大,用单个数据库完全可以轻松应付。

Memcached(缓存)+MySQL+垂直拆分

MySQL 主从读写分离

由于数据库的写入压力增加,Memcached只能缓解数据库的读取压力。读写集中在一个数据库上让数据库不堪重负,大部分网站开始使用主从复制技术来达到读写分离,以提高读写性能和读库的可扩展性。MySQL 的 Master-Slave 模式成为这个时候的网站标配了。

分表分库+水平拆分+MySQL集群

在 Memcached 的高速缓存,MySQL 的主从复制,读写分离的基础之上,这时 MySQL 主库的写压力开始出现瓶颈,而数据量的持续猛增,由于 MyISAM 使用表锁,在高并发下会出现严重的锁问题,大量的高并发 MySQL 应用开始使用 InnoDB 引擎代替MyISAM。

同时,开始流行使用分表分库来缓解写压力和数据增长的扩展问题。这个时候,分表分库成了一个热门技术,是面试的热门问题也是业界讨论的热门技术问题。也就在这个时候,MySQL 推出了还不太稳定的表分区,这也给技术实力一般的公司带来了希望。虽然 MySQL 推出了 MySQL Cluster 集群,但性能也不能很好满足互联网的要求,只是在高可靠性上提供了非常大的保证。

MySQL 的扩展问题

MySQL 数据库也经常存储一些大文本字段,导致数据库表非常的大,在做数据库恢复的时候就导致非常的慢,不容易快速恢复数据库。比如1000万 4KB 大小的文本就接近 40GB 的大小,如果能把这些数据从 MySQL 省去,MySQL 将变得非常的小。关系数据库很强大,但是它并不能很好的应付所有的应用场景。MySQL 的扩展性差(需要复杂的技术来实现),大数据下IO压力大,表结构更改困难,正是当前使用 MySQL 的开发人员面临的问题。

什么是NoSQL?

泛指非关系数据库,随着互联网 Web2.0 网站的兴起,传统的关系数据库在应付 Web2.0,特别是超大规模、高并发的 SNS 类型的纯动态网站已经力不从心,而非关系数据库凭借自身的特点得到了迅猛的发展。NoSQL 数据库的产生就是为了解决大规模数据集合多重数据种类带来的挑战,尤其是大数据应用难题,包括超大规模数据的存储。

3V+3高

大数据时代的 3V:

海量 Volume

多样 Variety

实时 Velocity

互联网需求的 3 高:

高并发

高可扩

高性能

CAP

C:强一致性(Consistency)

A:可用性(Availability)

P:分区容错性(Partition tolerance)

CAP 的 3 进 2

CAP 的理论就是说在分布式存储系统中,最多只能实现上面的两点,而由于当前的网络硬件肯定会出现延迟丢包问题,所以分区容忍性是我们必须实现的。

所以我们只能从一致性和可用性之间进行权衡,没有 NoSQL 能够同时保证三点。

CA:传统 Oracle 数据库

AP:大多数网站架构的选择

CP:Redis、Mongodb

BASE

基本可用(Basically Available)

软状态(Soft state)

最终一致(Eventually consistent)

它的思想是通过让系统放松对某一时刻数据一致性的要求来保证整体伸缩性和性能,原因就是大型系统往往由于地域分布和极高性能的要求,不可能采用分布式事务来完成这些指标,要想获得这些指标,我们必须采用一种方式来完成,这里 BASE 就是解决这个问题的办法。

分布式和集群

由多台计算机和通信软件通过计算机网络连接组成,分布式系统是建立在网络之上的软件系统,正是因为软件的特性,所以分布式系统具有高度的内聚性和透明性,因此,网络和分布式系统之间的区别更多的在于高层软件,而不是硬件,分布式系统可以应用在不同的平台。

分布式和集群的区别:

分布式:在多台不同的机器上部署不同的服务,它们之间使用 Rpc/Rml 之间通信和调用,对外提供服务和和组内协作。

集群:在多台不同的机器上部署相同的服务,通过分布式调用软件进行统一的调度,对外提供服务和访问。

Redis 入门介绍

什么是 Redis?

开源、底层采用 C语言、遵守 BSD 协议。

高性能 key/value 分布式内存数据库,基于内存运行。

支持持久化的 NoSQL 数据库。

和其他 NoSQL 相比,Redis 有三个特点:

支持数据的持久化,可以将内存中的数据持久化到磁盘中,重启之后可以再次使用。

不仅仅支持 key/value,还提供对 list、set、zset、hash 等数据类型的支持。

支持主从备份。

安装

将 Redis 安装包移动到 opt 目录下。

通过 tar -zxvf redis-3.0.4.tar.gz 解压。

进入解压后的目录执行 make,假如没有安装 gcc,就执行 yum install gcc-c++ 命令,然后运行 make distclean 之后执行 make。

make install。

其他

Redis 是一个单进程的。

它有16个数据库,默认为0号数据库。

SELECT:切换数据库。

DBSIZE:查看当前数据库的 key 的数量。

FLUSHDB:清空当前库。

FLUSHALL:清空所有库。

同意密码管理,16个库都是相同密码。

默认端口是 6379。

Redis 数据类型

Redis 键

keys * 查看当前库的所有 key

exists key 判断 key 是否存在

move key db 将当前库的某个 key 移到其他库

expire key 设置 key 的过期时间

ttl key 查看 key 的剩余使用实现

type key 查看 key 的类型

String

String 是 Redis 最基本的类型,一个 key 对应一个 vlaue,String 是二进制安全的,所以它可以包含所有数据,value 最多可以包含512M数据。

// 设置一个值
SET k1 v1
// 获取key对应的value
GET k1
// 添加一个value
APPEND k1 
// 查看key对应的value的长度
STRLEN k1
// 每次增加1
INCR k1
// 每次减少1
DECR k1
// 每次增加2
INCRBY k1 2
// 每次减少2
DECRBY k1 2
// 获取范围内值
GETRANGE k1 0 2
// 设置键存活的时间 
SETEX k1 20 v1
// 设置时判断是否存在key,如果存在就不做改变
SETNX k1 v1
// 同时设置多个值,如果键存在,修改值
MSET k1 v1 k2 v2
// 同时获取多个值
MGET k1 k2
// 同时设置多个值,如果存在键,执行失败
MSETNX k1 v1 k2 v2
// 设置并返回设置的value
getset k1 v1

Hash

是一个键值对集合,类似与 Java 里的 Map<String, Object>。

KV 模式不变,但 V 是一个键值对。

// 设置一个user,user的id属性是11
HSET user id 1
// 获取 user 的 id 属性
HGET user id
// 设置一个user的多个值
HMSET user name kernel age 18
// 获取多个属性
HMGET user name age
// 获取所有属性
HGETALL
// 删除user中的name属性
HDEL user name
// 查看user有几个属性
HLEN user
// 查看user中是否存在name属性
HEXISTS user name
// 查看user的所有key
HKEYS user
// 查看user的所有value 
HVALS user
// 自增2
HINCRBY user age 2
// 增加小数
HINCRBYFLOAT user score 0。5
// 不存在才能往里放
HSETNX user email kernel@163。com

List

简单的字符串列表,按照插入顺序排序,底层是一个链表。

单键多值。

它是一个字符串链表,可以从左右两边添加。

如果键不存在,创建链表,键存在,增加值,弹出所有值,键就失效了。

操作头尾效率高,中间元素效率低。

// 从左边向key中压入值
LPUSH list01 1 2 3 4 5
// 从右边向 key 中压入值
RPUSH list02 1 2 3 4 5
// 从左边查看范围内的值
Lrange list01 0 -1
// 从左边弹栈
LPOP
// 从右边弹栈
RPOP
// 查看指定索引的值
LINDEX list01 2
// 查看指定key的长度
LLEN list01
// 从key中从左边删除3个3
LREM list01 3 3
LTRIM list01 0 3 截取0-3赋值给key
// 从前面的key右边弹出一个值赋值给后面key
RPOPLPUSH list01 list02 

Set

String 类型的无序集合,采用 HashTable 实现的。

单键多值。

// 添加多个值,重复值不添加
SADD set01 1 2 3 4 5 1 2 3 4 5
// 查看set01的值
SMEMBERS set01
// 查看set01是否存在5
SISMEMBER set01 5
// 查看set01的元素个数
SCARD set01
// 删除set01中的3
SREM set01 3
// 从set01中随机取3个数
SRANDMEMBER set01 3
// 随机弹栈
SPOP set01
// 将 set01 中的5 移动到 set02 中
SMOVE set01 set02 5
// 差集,在set01不在set02中的值
SDIFF set01 set02
// 交集
SINTER set01 set02
// 并集
SUNION set01 set02

Zset

set 和 zset 的异同点:

相同点:String 类型元素的集合,不允许重复的成员。

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

// 设置值
ZADD zset01 60 v1 70 v2 80 v3 90 v4 100 v5
// 范围内取值,withscores表示包含score
ZRANGE zset01 0 -1
// 范围内取值包括score
// 区间内取值,(表示不包含,limit 开始下标,走多少步
ZRANGEBYSCORE zset01 60 80
// 删除某score对应的value
ZREM zset01 v4
// 查看zset01的个数
ZCARD zset01
// 查看区间内个数
ZCOUNT zset01 60 90
// 取下标
ZBANK zset01 60
// 取v4对应的值
ZSCORE zset01 v4
// 逆序获得v3对应的下标
ZREVRANK zset01 v3
// 逆序取范围内的值
ZREVRANGE zset01 0 -1
// 逆序区间内取值
ZREVRANGEBYSCORE zset01 90 60

解析配置文件 redis.conf

units 单位

1k => 1000 bytes 1kb => 1024 bytes 1m => 1000000 bytes 1mb => 10241024 bytes 1g => 1000000000 bytes 1gb => 10241024*1024 bytes

INCLUDES 包含

可以通过 includes 包含,redis.conf 可以作为总闸,包含其他配置。

GENERAL 通用

daemonize:配置作为守护进程运行。

pidfile:如以后台进程运行,Redis 将会把 pid 写入到 /var/run/redis.pid 文件,可以指定文件。

bind 127.0.0.1:绑定的主机地址,如果想全部访问不做限制的话,全为0.0.0.0。

timeout 300:当客户端闲置多长时间后关闭连接,如果指定为0,表示关闭该功能。

loglevel verbose:日志等级,有 debug、verbose、notice、warning,默认是 verbose。

logfile "" :日志文件保存位置,如果配置Redis为守护进程方式运行,而这里又配置为日志记录方式为标准输出,则日志将会发送给 /dev/null。

tcp-keepalive:单位为秒,如果设置为0,则不会进行 Keepalive 检测,建议设置成60。

syslog-enabled:是否把日志输出到 syslog 中。

syslog-ident:指定 syslog 里的日志标志。

syslog-facility:指定 syslog 设备,值可以是USER或 LOCAL0-LOCAL7。

SNAPSHOTTING 快照

Save 默认出厂设置如果1分钟写了10000次、5分钟写了10次、15分钟写了1次都会触发备份,如果想禁用 RDB 持久化策略,将 save 设为 save ""。

stop-writes-on-bgsave-error:如果配置成 no,表示你不在乎数据不一致或者有其他的手段发现和控制。

rdbcompression:对于存储到磁盘中的快照,可以设置是否进行压缩存储。如果是的话,Redis 会采用 LZF 算法进行压缩。如果你不想消耗 CPU 来进行压缩的话,可以设置为关闭此功能。

rdbchecksum:在存储快照后,还可以让 Redis 使用 CRC64 算法来进行数据校验,但是这样做会增加大约10%的性能消耗,如果希望获取到最大的性能提升,可以关闭此功能。

dbfilename

dir

SECURITY 安全

CONFIG SET requirepass 123456 将密码改为 123456,默认为空,设置后,执行任何命令之前必须执行 auth 123456。

LIMITS 限制

maxclients 最大连接客户端数量

maxmemory 设置 Redis 可以使用的内存量。一旦到达内存使用上限,Redis 将会试图移除内部数据。

maxmemory-policy :

volatile-lru:使用 LRU 算法移除 key,只对设置了过期时间的键。 allkeys-lru:使用 LRU 算法移除 key。 volatile-random:在过期集合中移除随机的 key,只对设置了过期时间的键。 allkeys-random:移除随机的 key。 volatile-ttl:移除那些 TTL 值最小的 key,即那些最近要过期的 key。 noeviction:不进行移除。针对写操作,只是返回错误信息。

maxmemory-samples:设置样本数量,LRU 算法和最小 TTL 算法都并非是精确的算法,而是估算值,所以你可以设置样本的大小,Redis 默认会检查这么多个 key 并选择其中 LRU 的那个。

APPEND ONLY MODE 追加

appendonly:配置 AOF 持久化。

appendfilename:持久化文件名。

appendfsync:同步

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

no-appendfsync-on-rewrite:重写时是否可以运用 Appendfsync,用默认 no 即可,保证数据安全性。

auto-aof-rewrite-min-size:设置重写的基准值。

常用配置

参数说明 redis.conf 配置项说明如下:

  1. Redis 默认不是以守护进程的方式运行,可以通过该配置项修改,使用 yes 启用守护进程。 daemonize no
  2. 当 Redis 以守护进程方式运行时,Redis 默认会把 pid 写入 /var/run/redis.pid 文件,可以通过 pidfile 指定。 pidfile /var/run/redis.pid
  3. 指定 Redis 监听端口,默认端口为6379。 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 \<dbid>命令在连接上指定数据库 id。 databases 16
  9. 指定在多长时间内,有多少次更新操作,就将数据同步到数据文件,可以多个条件配合。 save \<seconds> \<changes> 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. 设置当本机为 Slave 服务时,设置 Master 服务的 IP 地址及端口,在 Redis 启动时,它会自动从 Master 进行数据同步。 slaveof \<masterip> \<masterport>
  14. 当 Master 服务设置了密码保护时,Slave 服务连接 Master 的密码。 masterauth \<master-password>
  15. 设置 Redis 连接密码,如果配置了连接密码,客户端在连接 Redis 时需要通过 AUTH \<password> 命令提供密码,默认关闭。 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 \<bytes>
  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 到磁盘上,访问多的页面由磁盘自动换出到内存中。 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. 指定是否激活重置哈希,默认为开启。 activerehashing yes
  30. 指定包含其它的配置文件,可以在同一主机上多个Redis实例之间使用同一份配置文件,而同时各个实例又拥有自己的特定配置文件 include /path/to/local.conf

Redis 的持久化

RDB

Redis 会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何 IO 操作的,这就确保了极高的性能,如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那 RDB 方式要比 AOF 方式更加的高效。RDB 的缺点是最后一次持久化后的数据可能丢失。

RDB 保存的是 dump.rdb 文件。

Fork:

作用是复制一个与当前进程一样的进程,新进程的所有数据都和原进程一样,但是是一个新的进程,并作为原进程的子进程。

配置位置

在 redis.conf 文件中的 SNAPSHOTTING 下面。

如何触发 RDB 快照?

按照配置触发。

执行 save 或者 bgsave,save 只管保存,其他全部堵塞,bgsave 是后台异步进行快照,还可以接收用户请求。

执行 flushall 命令也会触发,但是由于执行了该命令清空所有库的数据,所有 dump.rdb 文件中是空的。

优势:

适合大规模的数据恢复。

对数据完整性和一致性要求不高。

劣势:

在一定间隔时间做一次备份,所以如果 Redis 意外 down 掉的话,就会丢失最后一次快照后的所有修改。

fork 的时候,内存中的数据被克隆了一份,大致2倍的膨胀性需要考虑。

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

总结:

RDB 是一个非常紧凑的文件,在保存 RDB 文件的时候父进程会 fork 出一个子进程,接下来全部工作由子进程负责,父进程不需要做任何 IO 操作,所有 RDB 方式可以最大化 Redis 的性能,与 AOF 相比,它更适合做大规模的数据回复,对数据完整性、一致性要求不高,数据丢失风险高,因为该方式 fork 子进程来持久化,当数据量大的时候,fork 的过程非常耗时,可能会导致在毫秒级上 Redis 不能响应客户端请求。

AOF

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

AOF 保存的是 appendonly.aof 文件。

配置位置

APPEND ONLY MODE,将 appendonly no 改为 yes。

AOF 启动/修复/恢复

正常恢复:

将有数据的 AOF 文件复制一份保存到对应目录 (config get dir)。 恢复:重启 Redis 然后重新加载。

异常恢复:

备份被写坏的 AOF 文件。 redis-check-aof --fix 文件名 进行修复。 恢复:重启 Redis 然后重新加载。

rewrite:

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

原理:

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

触发机制:

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

优势

每秒同步:appendfsync always 同步持久化每次发生数据变更会被立即记录到磁盘,性能较差但数据完整性比较好。

每修改同步:appendfsync everysec 异步操作,每秒记录,如果一秒内宕机,有数据丢失。

不同步:appendfsync no 从不同步。

劣势

相同数据集的数据而言 AOF 文件要远大于 RDB 文件,恢复速度慢于 RDB。

总结

AOF 文件是一个只进行追加的日志文件,Redis 可以在 AOF 文件体积过大时进行重写,AOF 文件有序的保存了对数据的写操作,这些写操作以 Redis 协议被很好的保存,因此,AOF 文件的内容非常容易被读懂,对文件的分析也很容易。对于相同的数据集来说,AOF 的体积通常要大于 RDB,根据所使用的 fsync 策略,AOF 的速度要慢于 RDB。

总结

RDB 持久化方式能够在指定的时间间隔能对你的数据进行快照存储。

AOF 持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF 命令以 Redis 协议追加保存每次写的操作到文件末尾。Redis 还能对 AOF 文件进行后台重写,使得 AOF 文件的体积不至于过大。

只做缓存

如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化方式。

同时开启两种持久化方式

在这种情况下,当 Redis 重启的时候会优先载入 AOF 文件来恢复原始的数据,因为在通常情况下 AOF 文件保存的数据集要比 RDB 文件保存的数据集要完整。

RDB 的数据不实时,同时使用两者时服务器重启也只会找 AOF 文件。

那要不要只使用 AOF 呢?

建议不要,因为 RDB 更适合用于备份数据库 (AOF在不断变化不好备份),快速重启,而且不会有 AOF 可能潜在的 bug,留着作为一个万一的手段。

性能建议

因为 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 文件,载入较新的那个。新浪微博就选用了这种架构。

Redis 的事务

悲观锁、乐观锁

悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会 block 直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。

乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,乐观锁策略:提交版本必须大于记录当前版本才能执行更新。

常用命令:

DISCARD 取消事务,放弃执行事务块内的所有命令

EXEC 执行所有事务块内的命令

MULTI 标记一个事务块的开始

UNWATCH 取消 WATCH 命令对所有 key 的监视

WATCH key [key...] 监视一个或多个 key,如果在事务执行之前这个 key 被其他命令所改动,那么事务将被打断

四种用法

正常执行:

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set k2 v22
QUEUED
127.0.0.1:6379> set k3 v33
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) OK

放弃事务:

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> DISCARD
OK
127.0.0.1:6379> get k3
"v33"

全体连坐:

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> setget k2 v2
(error) ERR unknown command 'setget'
127.0.0.1:6379> EXEC
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get k3
"v33"

怨头债主:

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set k2  v2
QUEUED
127.0.0.1:6379> INCR k2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) (error) ERR value is not an integer or out of range
3) OK

WATCH 监控:

对某个 key 监控之后,如果有别的客户端将这个 key 修改,则事务执行失败。

UNWATCH 的作用是放弃监控。

一旦执行 EXEC 无论成功与否所有监控所都被取消掉了。

WATCH 命令相当于乐观锁,事务提交时,如果 key 已经被修改,整个事务队列将执行失败,如果监视了多个 key,只要其中有一个 key 被修改,则整个队列执行失败,同时返回 Nullmulti-bulk 通知调用者事务执行失败。

三个阶段

开启:以MULTI开始一个事务

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

执行:由EXEC命令触发事务

三个特性

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

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

不保证原子性:Redis 同一个事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚。

Redis 的发布和订阅

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

首先需要订阅者订阅才可以收到消息,SUBSCRIBE c1 c2 c3。

然后发布者向频道发送消息,订阅者就可以收到了,比如 PUBLISH c3 word。

订阅者订阅频道时候还可以使用通配符,例如 SUBSCRIBE new*。

这样无论向 new1 频道还是 new2 频道发送消息订阅者都可以收到。

Redis 的主从复制

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

作用

读写分离

容灾恢复

配置过程

配从不配主。

从库配置:slaveof 主库 IP 主库端口,没次与主库断开连接后,都需要重新连接,除非写入到配置文件中。可以通过 info replication 查看当前数据库的配置。

常用三种方式:

一主二仆:

一个 Master 配置两个 Slave。Salve 只能读,不可以写,当 Master 挂掉,Salve 原地等待,Master 恢复后,自动连接,如果 Slave 挂掉,需要手动重新连接。

薪火相传:Master 配置一个 Slave,Slave 可以是下一个 Salve 的 Master,有效减轻 Master 的压力。

反客为主:通过 Salve no one 使当前数据库停止与其他数据库的同步,转成 Master。

复制原理

Slave 启动成功连接到 Master 后会发送一个 sync 命令。

Master 接到命令启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行完毕之后,Master 将传送整个数据文件到 Slave,以完成一次完全同步。

全量复制:而 Slave 服务在接收到数据库文件数据后,将其存盘并加载到内存中。

增量复制:Master 继续将新的所有收集到的修改命令依次传给 Slave,完成同步。

但是只要是重新连接 Master,一次完全同步(全量复制)将被自动执行。

哨兵模式

反客为主的自动版,监控主库是否挂掉,如果挂掉自动选出一个从库作为主库。

新建 sentinel.conf 文件,配置哨兵, sentinel monitor 被监控数据库名字 127.0.0.1 6379 1,1表示主机挂掉 Salve 投票看谁能当主机,得票多的当主机。

启动哨兵,redis-sentinel sentinel.conf 。

如果原有的 Master 挂掉,会自动选举出一个 Master,当 Master 恢复后,原 Master 作为 新 Master 的 Salve。

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

Redis 的 Java 客户端 Jedis

连接远程 Redis

  1. 开放端口 firewall-cmd --zone=public --add-port=6379/tcp --permanent
  2. 配置阿里云安全组规则
  3. 查看开放端口 firewall-cmd --zone=public --list-ports
  4. 启动 Redis 服务

Jedis 所需要的 jar

commons-pool-1.6.jar

jedis-2.1.0.jar

JedisPool

获取 Jedis 实例需要从 JedisPool 中获取,用完要返还给 JedisPool,出错也要返还给 JedisPool。

自定义 JedisPoolUtil

public class JedisPoolUtil {
    // 保证此变量对所有的线程的可见性,禁止指令重排序优化
    private static volatile JedisPool jedisPool = null;

    // 私有化构造方法
    private JedisPoolUtil() {
    }

    // 提供池子,采用双重检验锁机制
    public static JedisPool getJedisPool() {
        if (null == jedisPool) {
            synchronized (JedisPool.class) {
                if (null == jedisPool) {
                    JedisPoolConfig poolConfig = new JedisPoolConfig();
                    // 最大连接数
                    poolConfig.maxActive = 1000;
                    // 最大等待连接数
                    poolConfig.maxIdle = 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 (jedis != null) {
            jedisPool.returnResourceObject(jedis);
        }
    }
}

双重检验锁是对同步块加锁的方法,为什么教双重检验呢,那是因为有两次判空操作。

为何在同步块外面判空?

为了提高性能,如果去掉同步块外面判空,只要调用此方法,都会拿到一个静态内部锁,降低了效率,所以在外面加判空,降低了同步块的执行次数。

为什么还在同步块内判空呢?

可能会有多个线程同时进入同步块外面的判空,但是有线程先一步拿到了对象,如果不再内部判空,可能会生成多个实例。

一定要使用 volatile 修饰静态变量,否则由于多线程,还是会创建多个实例。

池化配置

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 对一些参数的默认设置如下: testWhileIdle=true minEvictableIdleTimeMills=60000 timeBetweenEvictionRunsMillis=30000 numTestsPerEvictionRun=-1

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券