一、Redis介绍
1.1 Redis(Remote dictionary Server)简介
用C语言编写的,遵守BSD协议,是一个高性能的(key/value)分布式内存数据库,基于内存运行并支持持久化的NoSQL数据库,是当前最热门的NoSql数据库之一,也被称为数据结构服务器。
1.2 Redis特点
Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使
Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储Redis支持数据的备份,即master-slave模式的数据备份
1.3 Redis应用
内存存储和持久化:redis支持异步将内存中的数据写到硬盘上,同时不影响继续服务取最新N个数据的操作,如:
可以将最新的10条评论的ID放在Redis的List集合里面模拟类似于HttpSession这种需要设定过期时间的功能发布、订阅消息系统定时器、计数器
1.4 Redis下载
- Http://www.redis.cn/
二、Redis安装和环境配置
Win安装
1.下载网址:https://github.com/dmajkic/redis/downloads
2.下载到的Redis支持32bit和64bit
根据自己实际情况选择,将64bit的内容cp到自定义盘符安装目录取名redis。 如 C:\redis
3.运行 redis-server.exe redis.conf
打开一个cmd窗口 使用cd命令切换目录到 C:\redis,然后运行redis.conf,如果想方便的话,可以把redis的路径加到系统的环境变量里,这样就省得再输路径了,后面的那个redis.conf可以省略,如果省略,会启用默认的。输入之后,会显示如下界面:
4.运行客户端和服务器
- 这时候另启一个cmd窗口,原来的不要关闭,不然就无法访问服务端了。切换到redis目录下运行 redis-cli.exe -h 127.0.0.1 -p 6379 。设置键值对 set myKey abc取出键值对 get myKey
Linux安装(推荐)
1. 下载获得redis-3.0.4.tar.gz后将它放入Linux目录/opt
2. /opt目录下,解压命令:tar -zxvf redis-3.0.4.tar.gz
3. 在redis-3.0.4目录下执行make命令
4. 如果make完成后继续执行make install
5. 查看默认安装目录:usr/local/bin
- Redis-benchmark:性能测试工具
- Redis-check-aof:修复有问题的AOF文件
- Redis-check-dump:修复有问题的dump.rdb文件
- Redis-cli:客户端,操作入口
- Redis-sentinel:redis集群使用
-
6. 启动客户端和服务器
修改redis.conf文件将里面的daemonize no 改成 yes,让服务在后台启动 将默认的redis.conf拷贝到自己定义好的一个路径下,比如/myconf 进入/usr/local/bin目录
- 服务器启动:redis-server /myredis/redis.conf
- 客户端启动:redis-cli -p 6379
7. 关闭
- 单实例关闭:redis-cli shutdown exit
- 多实例关闭,指定端口关闭:redis-cli -p 6379 shutdown
三、Redis基础知识讲解
3.1 单进程
- Redis是单进程处理客户端请求,对读写等时间的响应通过对epoll函数的包装做到的,Redis的实际处理速度完全依靠主进程的执行效率。
Epoll是Linux内核为处理大批量文件描述符而作了改进的epoll 是Linux下多路复用IO接口select/poll的增强版本, 它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。
3.2 数据库切换
select 下标[0-15],数据会存储在对应的库中,最终在文件磁盘上。
3.3 查看数据库中的key数量
-
3.4 查看所有的key
keys *,keys k??(?代表占位)
3.5 清除当前库
四、Redis常用五大数据结构
4.1 key(关键字)
- keys *,keys k??:查看key
- EXIST key名:查看对应的key是否存在
- move key名 索引:把当前key移动到对应索引数据库
- ttl key名:查看key的生命周期
- expire key名 数字n:给key设置生命周期为n秒,过期后key被移除。
delete key名:删除对应的key type key名:查看key的数据类型
4.2 String(字符串)
string是redis最基本的类型,你可以理解成与Memcached一模一样的类型,一个key对应一个value。
- string类型是二进制安全的。意思是redis的string可以包含任何数据。比如jpg图片或者序列化的对象 。
- string类型是Redis最基本的数据类型,一个redis中字符串value最多可以是512M
- Append:key末尾添加字符串
- Strlen:查看key对应字符串长度
- Incr/Decr/incrby/decrby:增减,只能对数字操作,incr/decr key名(每次增减1)和incyby key名 数字(每次增减对应数字)
- getrange/setrange:设置key的范围,getrange key名 0 3从索引0开始获取3个字符,setrange key名 0 xxx在第0个索引处插入xxx
- setex(set with expire):键秒值,设置生命周期,set key名 n秒 value值
- setnx:只有对不存在的key才能使用,同样是设置带生命周期的value值
- mset/mget/msetnx:合并获取/设置,其中msetnx是对不存在的key才使用(一但有一个key存在,则语句失效)
4.3 Hash(哈希,类似于HashMap)
- Redis hash 是一个键值对集合。Redis hash是一个string类型的field和value的映射表,hash特别适合用于存储对象。类似Java里面的Map
- KV模式不变,但V是一个键值对
- hset/hget/hmset/hmget/hgetall/hdel:设置/获取/删除 键值对
- hlen:获取哈希的长度
- hexists hash名 hash中的值的key:hash中的key是否存在
- hkeys/hvals hash名:查看所有的key/value
- hincrby/hincrbyfloat n:自增n
hsetnx key值 value:如果hash中不存在对应的key,则设置对应的key字段
4.4 List(列表)
Redis 列表是简单的字符串列表,按照插入顺序排序。可以添加一个元素导列表的头部(左边)或者尾部(右边)。它的底层实际是个链表
- lpush/rpush(上左下右) list名 元素(1 2 3 4 5):往list中(左1 2 3 4 5 /右5 4 3 2 1)添加元素(入队)
- lrange list名 0 -1:按顺序显示list中的元素,从队尾出
- lpop/rpop(上左下右):每次出一个栈顶/底元素
- lindex list名 索引值:显示list对应索引值的元素(从上到下)
- llen list名:list的长度
- lrem key n个 数字m:删除n个value(m),删除顺序从上到下
- ltrim llist名 startIndex endIndex:截取指定范围的值后(从上到下),赋给list
- rpoplpush 源list 目的list:从源list的栈底出栈到目的list的栈顶
- lset list名 index value:对list的某下标设置对应值
- linsert list名 before/after 值1 值2:在list中的值1前/后插入一个值2
性能总结:
Redis的Set是string类型的无序集合。它是通过HashTable实现实现的
- sadd/smembers/sismember:在set中添加元素/查看元素/查看是否是set中的元素
- scard:获取集合中的元素个数
- srem set名 value:删除set中的元素
- srandmember key n:set中随机出现n个几个数
- spop set名:set中随机出栈
- smove set1 set2 值:把set1中的对应值移动到set2
- sdiff set1 set2:差集,在set1中但不在set2中
- sinter:交集
-
4.6 Zset(sorted set有序集合)
Redis zset 和 set 一样也是string类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个double类型的分数。
- redis正是通过分数来为集合中的成员进行从小到大的排序。zset的成员是唯一的,但分数(score)却可以重复。
- 在set的基础上加一个score值,例如zset k1 score1,k2 score2
- zadd/zrange(withscores):添加zset中的key和对应的分数,在zrange中如果有withscores会显示对应的score
- zrangebyscore zset名 开始score 结束score:根据score的返回显示zset的对应值,(表示包含,尾部使用limit n表示限制n个结果
- zrem zset名 value值:删除zset中的value和对应的score
- zcard/zcount zset名 score区间/:获得zset中对应分数的value个数
- zrank zset名 value值:获得zset的对应值的下标
- zscore zset名 value:获得value值对应的score
- zrevrank key values值:逆序获得下标志
- zrevrange key 0 -1:逆序显示zset中的value
五、Redis配置文件
5.1 redis配置文件
- 目前存放在/opt/redis.conf
-
5.2 Units单位
1k = 1000bytes
- 1kb = 1024bytes
-
5.2 Include
包含其他的配置文件,例如:inlude /path/local.conf
5.3 General
常用的设置,比如port端口号,daemonize pidfile
5.4 Security
安全设置,例如密码设置等
- config get dir:获取当前redis的启动目录,很多redis打印信息可能会在启动目录下打印
config set/get requirepass:设置/获取redis的进入密码
5.5 Limit极限
Maxclients:最多客户端数 - 10000
- Maxmemory:最大内存,有过期策略,当达到最大内存时会根据过期策略移除内存中的数据
- (1)volatile-lru:使用LRU(最近最少使用)算法移除key,只对设置了过期时间的键
- (2)allkeys-lru:使用LRU算法移除key
- (3)volatile-random:在过期集合中移除随机的key,只对设置了过期时间的键
- (4)allkeys-random:移除随机的key
- (5)volatile-ttl:移除那些TTL值最小的key,即那些最近要过期的key
- (6)noeviction:不进行移除(默认永不过期,但不建议实际使用)。针对写操作,只是返回错误信息
Maxmemory-samples:设置样本数量,LRU算法和最小TTL算法都并非是精确的算法,而是估算值,所以你可以设置样本的大小,redis默认会检查这么多个key并选择其中LRU的那个
5.6 常用配置
- Redis默认不是以守护进程的方式运行,可以通过该配置项修改,使用yes启用守护进程
- daemonize no
- Redis默认不是以守护进程的方式运行,可以通过该配置项修改,使用yes启用守护进程
- 当Redis以守护进程方式运行时,Redis默认会把pid写入/var/run/redis.pid文件,可以pidfile指定
- pidfile /var/run/redis.pid
- 当Redis以守护进程方式运行时,Redis默认会把pid写入/var/run/redis.pid文件,可以pidfile指定
- 指定Redis监听端口,默认端口为6379,作者在自己的一篇博文中解释了为什么选用6379作为默认端口,因为6379在手机按键上MERZ对应的号码,而MERZ取自意大利歌女Alessia Merz的名字
- port 6379
- 指定Redis监听端口,默认端口为6379,作者在自己的一篇博文中解释了为什么选用6379作为默认端口,因为6379在手机按键上MERZ对应的号码,而MERZ取自意大利歌女Alessia Merz的名字
- 绑定的主机地址
- bind 127.0.0.1
- 绑定的主机地址
- 5.当客户端闲置多长时间后关闭连接,如果指定为0,表示关闭该功能
- timeout 300
- 指定日志记录级别,Redis总共支持四个级别:debug、verbose、notice、warning,默认为verbose
- loglevel verbose
- 指定日志记录级别,Redis总共支持四个级别:debug、verbose、notice、warning,默认为verbose
- 日志记录方式,默认为标准输出,如果配置Redis为守护进程方式运行,而这里又配置为日志记录方式为标准输出,则日志将会发送给/dev/null
- logfile stdout
- 日志记录方式,默认为标准输出,如果配置Redis为守护进程方式运行,而这里又配置为日志记录方式为标准输出,则日志将会发送给/dev/null
- 设置数据库的数量,默认数据库为0,可以使用SELECT
命令在连接上指定数据库id - databases 16
- 设置数据库的数量,默认数据库为0,可以使用SELECT
- 指定在多长时间内,有多少次更新操作,就将数据同步到数据文件,可以多个条件配合
- save
- Redis默认配置文件中提供了三个条件:
- save 900 1
- save 300 10
- save 60 10000
- 分别表示900秒(15分钟)内有1个更改,300秒(5分钟)内有10个更改以及60秒内有10000个更改。
- save
- 指定在多长时间内,有多少次更新操作,就将数据同步到数据文件,可以多个条件配合
- 指定存储至本地数据库时是否压缩数据,默认为yes,Redis采用LZF压缩,如果为了节省CPU时间,可以关闭该选项,但会导致数据库文件变的巨大
- rdbcompression yes
- 指定存储至本地数据库时是否压缩数据,默认为yes,Redis采用LZF压缩,如果为了节省CPU时间,可以关闭该选项,但会导致数据库文件变的巨大
- 指定本地数据库文件名,默认值为dump.rdb
- dbfilename dump.rdb
- 指定本地数据库文件名,默认值为dump.rdb
- 指定本地数据库存放目录
- dir ./
- 指定本地数据库存放目录
- 设置当本机为slav服务时,设置master服务的IP地址及端口,在Redis启动时,它会自动从master进行数据同步
- slaveof
- slaveof
- 设置当本机为slav服务时,设置master服务的IP地址及端口,在Redis启动时,它会自动从master进行数据同步
- 当master服务设置了密码保护时,slav服务连接master的密码
- masterauth
- masterauth
- 当master服务设置了密码保护时,slav服务连接master的密码
- 设置Redis连接密码,如果配置了连接密码,客户端在连接Redis时需要通过AUTH
命令提供密码,默认关闭 - requirepass foobared
- 设置Redis连接密码,如果配置了连接密码,客户端在连接Redis时需要通过AUTH
- 设置同一时间最大客户端连接数,默认无限制,Redis可以同时打开的客户端连接数为Redis进程可以打开的最大文件描述符数,如果设置 maxclients 0,表示不作限制。当客户端连接数到达限制时,Redis会关闭新的连接并向客户端返回max number of clients reached错误信息
- maxclients 128
- 设置同一时间最大客户端连接数,默认无限制,Redis可以同时打开的客户端连接数为Redis进程可以打开的最大文件描述符数,如果设置 maxclients 0,表示不作限制。当客户端连接数到达限制时,Redis会关闭新的连接并向客户端返回max number of clients reached错误信息
- 指定Redis最大内存限制,Redis在启动时会把数据加载到内存中,达到最大内存后,Redis会先尝试清除已到期或即将到期的Key,当此方法处理 后,仍然到达最大内存设置,将无法再进行写入操作,但仍然可以进行读取操作。Redis新的vm机制,会把Key存放内存,Value会存放在swap区
- maxmemory
- maxmemory
- 指定Redis最大内存限制,Redis在启动时会把数据加载到内存中,达到最大内存后,Redis会先尝试清除已到期或即将到期的Key,当此方法处理 后,仍然到达最大内存设置,将无法再进行写入操作,但仍然可以进行读取操作。Redis新的vm机制,会把Key存放内存,Value会存放在swap区
- 指定是否在每次更新操作后进行日志记录,Redis在默认情况下是异步的把数据写入磁盘,如果不开启,可能会在断电时导致一段时间内的数据丢失。因为 redis本身同步数据文件是按上面save条件来同步的,所以有的数据会在一段时间内只存在于内存中。默认为no
- appendonly no
- 指定是否在每次更新操作后进行日志记录,Redis在默认情况下是异步的把数据写入磁盘,如果不开启,可能会在断电时导致一段时间内的数据丢失。因为 redis本身同步数据文件是按上面save条件来同步的,所以有的数据会在一段时间内只存在于内存中。默认为no
- 指定更新日志文件名,默认为appendonly.aof
- appendfilename appendonly.aof
- 指定更新日志文件名,默认为appendonly.aof
- 指定更新日志条件,共有3个可选值:
- no:表示等操作系统进行数据缓存同步到磁盘(快)
- always:表示每次更新操作后手动调用fsync()将数据写到磁盘(慢,安全)
- everysec:表示每秒同步一次(折衷,默认值)
- appendfsync everysec
- 指定更新日志条件,共有3个可选值:
- 指定是否启用虚拟内存机制,默认值为no,简单的介绍一下,VM机制将数据分页存放,由Redis将访问量较少的页即冷数据swap到磁盘上,访问多的页面由磁盘自动换出到内存中(在后面的文章我会仔细分析Redis的VM机制)
- vm-enabled no
- 指定是否启用虚拟内存机制,默认值为no,简单的介绍一下,VM机制将数据分页存放,由Redis将访问量较少的页即冷数据swap到磁盘上,访问多的页面由磁盘自动换出到内存中(在后面的文章我会仔细分析Redis的VM机制)
- 虚拟内存文件路径,默认值为/tmp/redis.swap,不可多个Redis实例共享
- vm-swap-file /tmp/redis.swap
- 虚拟内存文件路径,默认值为/tmp/redis.swap,不可多个Redis实例共享
- 将所有大于vm-max-memory的数据存入虚拟内存,无论vm-max-memory设置多小,所有索引数据都是内存存储的(Redis的索引数据 就是keys),也就是说,当vm-max-memory设置为0的时候,其实是所有value都存在于磁盘。默认值为0
- vm-max-memory 0
- 将所有大于vm-max-memory的数据存入虚拟内存,无论vm-max-memory设置多小,所有索引数据都是内存存储的(Redis的索引数据 就是keys),也就是说,当vm-max-memory设置为0的时候,其实是所有value都存在于磁盘。默认值为0
- Redis swap文件分成了很多的page,一个对象可以保存在多个page上面,但一个page上不能被多个对象共享,vm-page-size是要根据存储的 数据大小来设定的,作者建议如果存储很多小对象,page大小最好设置为32或者64bytes;如果存储很大大对象,则可以使用更大的page,如果不 确定,就使用默认值
- vm-page-size 32
- Redis swap文件分成了很多的page,一个对象可以保存在多个page上面,但一个page上不能被多个对象共享,vm-page-size是要根据存储的 数据大小来设定的,作者建议如果存储很多小对象,page大小最好设置为32或者64bytes;如果存储很大大对象,则可以使用更大的page,如果不 确定,就使用默认值
- 设置swap文件中的page数量,由于页表(一种表示页面空闲或使用的bitmap)是在放在内存中的,,在磁盘上每8个pages将消耗1byte的内存。
- vm-pages 134217728
- 设置swap文件中的page数量,由于页表(一种表示页面空闲或使用的bitmap)是在放在内存中的,,在磁盘上每8个pages将消耗1byte的内存。
- 设置访问swap文件的线程数,最好不要超过机器的核数,如果设置为0,那么所有对swap文件的操作都是串行的,可能会造成比较长时间的延迟。默认值为4
- vm-max-threads 4
- 设置访问swap文件的线程数,最好不要超过机器的核数,如果设置为0,那么所有对swap文件的操作都是串行的,可能会造成比较长时间的延迟。默认值为4
- 设置在向客户端应答时,是否把较小的包合并为一个包发送,默认为开启
- glueoutputbuf yes
- 设置在向客户端应答时,是否把较小的包合并为一个包发送,默认为开启
- 指定在超过一定的数量或者最大的元素超过某一临界值时,采用一种特殊的哈希算法
- hash-max-zipmap-entries 64
- hash-max-zipmap-value 512
- 指定在超过一定的数量或者最大的元素超过某一临界值时,采用一种特殊的哈希算法
- 指定是否激活重置哈希,默认为开启(后面在介绍Redis的哈希算法时具体介绍
- activerehashing yes
- 指定是否激活重置哈希,默认为开启(后面在介绍Redis的哈希算法时具体介绍
- 指定包含其它的配置文件,可以在同一主机上多个Redis实例之间使用同一份配置文件,而同时各个实例又拥有自己的特定配置文件
- include /path/to/local.conf
- 指定包含其它的配置文件,可以在同一主机上多个Redis实例之间使用同一份配置文件,而同时各个实例又拥有自己的特定配置文件
六、Redis持久化
6.1 RDB(Redis DataBase)
- 内存中的数据集快照写入磁盘,(Snapshot快照),恢复就是将快照文件读取到内存
- Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能
- 如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失。
- Fork的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、程序计数器等)数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程
- RDB保存的是dump.rbd文件,修改dir 对应的目录,则执行shutdown save时,会自动在dir的目录保存dump.rbd文件(记录了之前redis中的数据) ,save设置就是RDB的快照生成策略,默认是900s 进行过1次修改或300s 进行10次修改或60s进行过1万次修改就触发,如果想让redis的rdb立即生效,则使用命令save即可。
- 如果进行了flushall等操作,也会将dump.rbd中的数据清空
- stop-writes-on-bgsave-error yes:后台存储出错时,要停止写入
- rdbcompression:对于存储到磁盘中的快照,可以设置是否进行压缩存储。如果是的话,redis会采用LZF算法进行压缩。如果你不想消耗CPU来进行压缩的话,可以设置为关闭此功能
- rdbchecksum:在存储快照后,还可以让redis使用CRC64算法来进行数据校验,但是这样做会增加大约10%的性能消耗,如果希望获取到最大的性能提升,可以关闭此功能
- 优势:
- 适合大规模的数据恢复
- 对数据完整性和一致性要求不高
- 劣势:
- 在一定间隔时间做一次备份,所以如果redis意外down掉的话,就会丢失最后一次快照后的所有修改
- Fork的时候,内存中的数据被克隆了一份,大致2倍的膨胀性需要考虑
6.2 AOF(Append Only File)
- 以日志的形式来记录每个写操作,将Redis执行过的所有写指令记录下来(读操作不记录),只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作(如果操作中包括flushall,则aof文件中也有flushall,因此可能会造成数据库恢复为空)
- 在redis.conf中找到appendonly no,把no设置为yes即可。
- 对于rdb和aof同时存在时,redis会先找aof,如果aof文件出了问题则redis无法启动,使用redis-check-aof —fix appendonly.aof可以自动修复AOF文件
- appendonly:默认是no,yes表明打开
- appendfsync:
- always表示同步持久化,每次数据变更就立即记录到磁盘,性能差但完整性很好
- Everysec:出厂默认,异步操作,每秒记录
- no:不记录
- 正常恢复:
- 启动:修改默认的appendonly no,改为yes
- 将有数据的aof文件复制一份保存到对应目录(config get dir)
- 恢复:重启redis然后重新加载
- 异常恢复:(dump.rdb恢复也是一样)
- 修改默认的appendonly no,改为yes
- 备份被写坏的AOF文件
- Redis-check-aof —fix进行修复
- 恢复:重启redis然后重新加载
- Rewrite(重写):
- AOF采用文件追加方式,文件会越来越大为避免出现此种情况,新增了重写机制,当AOF文件的大小超过所设定的阈值时,Redis就会启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集.可以使用命令bgrewriteaof。
- 原理:AOF文件持续增长而过大时,会fork出一条新进程来将文件重写(也是先写临时文件最后再rename),遍历新进程的内存中数据,每条记录有一条的Set语句。重写aof文件的操作,并没有读取旧的aof文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的aof文件,这点和快照有点类似。
- 触发机制:Redis会记录上次重写时的AOF大小,默认配置是当AOF文件大小是上次rewrite后大小的一倍且文件大于64M时触发。
- No-appendfsync-on-rewrite:重写时是否可以运用Appendfsync,用默认no即可,保证数据安全性。
- Auto-aof-rewrite-min-size:设置重写的基准值
- Auto-aof-rewrite-percentage:设置重写的基准值
- 优势
- 每修改同步:appendfsync always 同步持久化 每次发生数据变更会被立即记录到磁盘 性能较差但数据完整性比较好
- 每秒同步:appendfsync everysec 异步操作,每秒记录 如果一秒内宕机,有数据丢失
- 不同步:appendfsync no 从不同步
- 劣势
- Aof运行效率要慢于rdb,每秒同步策略效率较好,不同步效率和rdb相同
- 相同数据集的数据而言aof文件要远大于rdb文件,恢复速度慢于rdb
6.3 总结
- 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事务—部分支持
7.1 介绍
7.2.2 放弃事务
7.2.3 全体连坐
- 整个事务执行的过程中,如果出现命令错误(执行错误),则全部操作失败,默认事务未完成(要么全成功,要么全失败)。
7.2.4 冤头债主
- 整个事务执行的过程中,命令成功,加入队列,但实际运行后是异常命令(比如给字母自增),那么在事务提交后只有对应的异常命令无法执行。
7.2.5 Watch监控
- 乐观锁:乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量
- 乐观锁策略:提交版本必须大于记录当前版本才能执行更新
- 举例:用户1和用户2同时操作当前行,用户1先修改时版本号为1,当用户1修改好后版本号为2,而用户2准备操作时版本号由之前的1变为2,不一致导致本次操作失败,因此用户2需要提交本次事务后,重新以版本2来进行修改,直到版本在整个操作过程中都一致才能保证成功(CAS思想)。
- 悲观锁:例如表锁(锁整表),一致性很好,但并发性较差,悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁
- 举例:
- 初始化信用卡可用余额和欠额
- 无加塞篡改,先监控再开启multi,保证两笔金额变动在同一个事务内
- 有加塞篡改:类似于乐观锁
- unwatch的情况
- Watch指令,类似乐观锁,事务提交时,如果Key的值已被别的客户端改变,比如某个list已被别的客户端push/pop过了,整个事务队列都不会被执行通过WATCH命令在事务执行之前监控了多个Keys,倘若在WATCH之后有任何Key的值发生了变化,EXEC命令执行的事务都将被放弃,同时返回Nullmulti-bulk应答以通知调用者事务执行失败
在需要修改key时,先使用watch监控,修改完成后在使用unwatch释放是推荐的方式。
7.3 事务三阶段
7.3.1 开启
-
7.3.2 入队
将多个命令入队到事务中,接到这些命令并不会立即执行,而是放到等待执行的事务队列里面
7.3.3 执行**
-
7.4 事务三特性
- 单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
- 没有隔离级别的概念:队列中的命令没有提交之前都不会实际的被执行,因为事务提交前任何指令都不会被实际执行,也就不存在”事务内的查询要看到事务里的更新,在事务外查询不能看到”这个让人万分头痛的问题
- 不保证原子性:redis同一个事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚
八、Redis发布订阅
- 发布订阅:进程间的一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。
- 命令
- 命令案例
- 先订阅后发布后才能收到消息,
- 可以一次性订阅多个,SUBSCRIBE c1 c2 c3
- 客户端订阅多个,通配符, PSUBSCRIBE new
- 收取消息, PUBLISH new1 redis2015
九、Redis复制机制
9.1 复制介绍
- 也就是主从复制,主机数据更新后根据配置和策略,自动同步到备机的master/slaver机制,Master以写为主,Slave以读为主
- 主要用于读写分离,容灾恢复。
- 配置细节:
- 配从不配主:slaveof 主库IP 主库端口,每次与master断开之后,都需要重新连接,除非你配置进redis.conf文件
- 拷贝多个redis.conf文件,并进行修改端口/pid号/日志文件/dump.rdb等为6379(不变) 6380 6381
- 常用三个方法
- 一主二仆:
- 一台主机两台从机,info replication可以查看当前server的角色(主/从),通过slaveof 主机ip 主机端口,可以把其中两台设置为从机
- 从设置为从机后,能够备份主机所有的数据
- 主机写,从机读,在主机写入key后,从机无法对主机写的key进行修改
- 主机shutdown后,两个从机的状态不变,仍然是slave,但连接状态是down,当主机再重新连接后,从机依然与之前的状态一样。
- 从机shutdown后,再重新连接后,就以默认的master方式连接(除非写进配置文件),需要重新与主机连接后才能生效
- 一主二仆:
- 薪火相传:去中心化,减轻主机负担,主->从->从,一台主机只管一台从机,而这台从机再管其他从机。
- 上一个Slave可以是下一个slave的Master,Slave同样可以接收其他slaves的连接和同步请求,那么该slave作为了链条中下一个的master,可以有效减轻master的写压力
- 中途变更转向:会清除之前的数据,重新建立拷贝最新的
- 方式:Slaveof 新主库IP 新主库端口
- 反客为主:主机挂了以后,从机上位为主机,使当前数据库停止与其他数据库的同步,转成主数据库
- Slave启动成功连接到master后会发送一个sync命令
- Master接到命令启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,
- 在后台进程执行完毕之后,master将传送整个数据文件到slave,以完成一次完全同步
- 全量复制(首次):而slave服务在接收到数据库文件数据后,将其存盘并加载到内存中。
- 增量复制:Master继续将新的所有收集到的修改命令依次传给slave,完成同步
- 但是只要是重新连接master,一次完全同步(全量复制)将被自动执行
9.3 哨兵模式—监控
9.3.1 介绍
- 调整结构,6379带着80、81
- 自定义的/myredis目录下新建sentinel.conf文件,名字绝不能错
- 配置哨兵sentinel.conf,填写内容:
- sentinel monitor 被监控数据库名字(自己起名字) 127.0.0.1 6379 1,例如sentinel monitor host6379 127.0.0.1 6379 1
- 上面最后一个数字1,表示主机挂掉后salve投票看让谁接替成为主机,得票数多少后成为主机
- 启动哨兵
- redis-sentinel /myredis/sentinel.conf (目录依照各自的实际情况配置,可能目录不同)
- 正常主从演示
- 原来的master(6379)挂了
- 投票新选
- 复制延迟:由于所有的写操作都是先在Master上操作,然后同步更新到Slave上,所以从Master同步到Slave机器有一定的延迟,当系统很繁忙的时候,延迟问题会更加严重,Slave机器数量的增加也会使这个问题更加严重。
十、Java连通Redis
- 导入jedis.jar和Commons-pool-1.6jar包
编写java代码连通redis
import redis.clients.jedis.Jedis;
public static void main(String[] args) {
// 创建redis客户端
Jedis jedis = new Jedis("127.0.0.1",6379);
// 测试redis连接
System.out.println(jedis.ping());
}
常用API测试—五大数据类型 ```java
package com.atguigu.redis.test;
import java.util.*;
import redis.clients.jedis.Jedis;
public class Test02 { public static void main(String[] args) {
Jedis jedis = new Jedis("127.0.0.1",6379);
//key
Set<String> keys = jedis.keys("*");
for (Iterator iterator = keys.iterator(); iterator.hasNext();) {
String key = (String) iterator.next();
System.out.println(key);
}
System.out.println("jedis.exists====>"+jedis.exists("k2"));
System.out.println(jedis.ttl("k1"));
//String
//jedis.append("k1","myreids");
System.out.println(jedis.get("k1"));
jedis.set("k4","k4_redis");
System.out.println("----------------------------------------");
jedis.mset("str1","v1","str2","v2","str3","v3");
System.out.println(jedis.mget("str1","str2","str3"));
//list
System.out.println("----------------------------------------");
//jedis.lpush("mylist","v1","v2","v3","v4","v5");
List<String> list = jedis.lrange("mylist",0,-1);
for (String element : list) {
System.out.println(element);
}
//set
jedis.sadd("orders","jd001");
jedis.sadd("orders","jd002");
jedis.sadd("orders","jd003");
Set<String> set1 = jedis.smembers("orders");
for (Iterator iterator = set1.iterator(); iterator.hasNext();) {
String string = (String) iterator.next();
System.out.println(string);
}
jedis.srem("orders","jd002");
System.out.println(jedis.smembers("orders").size());
//hash
jedis.hset("hash1","userName","lisi");
System.out.println(jedis.hget("hash1","userName"));
Map<String,String> map = new HashMap<String,String>();
map.put("telphone","13811814763");
map.put("address","atguigu");
map.put("email","abc@163.com");
jedis.hmset("hash2",map);
List<String> result = jedis.hmget("hash2", "telphone","email");
for (String element : result) {
System.out.println(element);
}
//zset
jedis.zadd("zset01",60d,"v1");
jedis.zadd("zset01",70d,"v2");
jedis.zadd("zset01",80d,"v3");
jedis.zadd("zset01",90d,"v4");
Set<String> s1 = jedis.zrange("zset01",0,-1);
for (Iterator iterator = s1.iterator(); iterator.hasNext();) {
String string = (String) iterator.next();
System.out.println(string);
}
} }
4. 事务
```java
public static void main(String[] args) {
Jedis jedis = new Jedis("127.0.0.1",6379);
// 事务开启
Transaction transaction = jedis.multi();
transaction.set("k1", "v1");
transaction.set("k100", "v100");
// 事务提交
transaction.exec();
// 事务放弃
// transaction.discard();
}
public boolean transMethod() throws InterruptedException {
Jedis jedis = new Jedis("127.0.0.1", 6379);
int balance;// 可用余额
int debt;// 欠额
int amtToSubtract = 10;// 实刷额度
jedis.watch("balance");
//jedis.set("balance","5");//此句不该出现,讲课方便。模拟其他程序已经修改了该条目
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);
}
主从复制:准备两台server分别运行在6379和6380端口
public static void main(String[] args) {
// 主机
Jedis master = new Jedis("127.0.0.1",6379);
// 从机
Jedis slave = new Jedis("127.0.0.1",6380);
// 主从连接
slave.slaveof("127.0.0.1", 6379);
// 主写/从读
master.set("k1", "v1");
System.out.println(slave.get("k1"));
}
jedisPool:jedis池,少建立/释放连接
- jedisconfig配置总结
- 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
- jedisconfig配置总结
// 双重检测单例模式
public class JedisPoolUtil {
private static JedisPool jedisPool = null;
private JedisPoolUtil(){
}
// 单例模式创建Jedispool
public static JedisPool getJedisPool(){
if(jedisPool == null){
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxActive(1000);
poolConfig.setMaxIdle(32);
poolConfig.setMaxWait(100);
jedisPool = new JedisPool(poolConfig, "127.0.0.1",6379);
}
return jedisPool;
}
// 把jedis对象放回pool中
public static void release(JedisPool pool,Jedis jedis){
if(jedis != null){
pool.returnResource(jedis);
}
}
}
public class TestPool {
public static void main(String[] args) {
JedisPool pool = JedisPoolUtil.getJedisPool();
Jedis jedis = null;
try {
jedis = pool.getResource();
jedis.set("aa", "bb");
} catch (Exception e) {
e.printStackTrace();
// TODO: handle exception
}finally {
System.out.println(jedis.get("aa"));
JedisPoolUtil.release(pool, jedis);
}
}
}
十一、Linux下Redis开机自启动
- 设置redis.conf中daemonize为yes,确保守护进程开启(vim /myredis/redis.conf vim模式下输入/daemonize 查找)。
PATH=/usr/local/bin:/sbin:/usr/bin:/bin
REDISPORT=6379
EXEC=/usr/local/bin/redis-server
REDIS_CLI=/usr/local/bin/redis-cli
PIDFILE=/var/run/redis.pid
CONF=”/usr/local/redis/etc/redis.conf”
AUTH=”123456”
case “$1” in
start)
if [ -f $PIDFILE ]
then
echo “$PIDFILE exists, process is already running or crashed.”
else
echo “Starting Redis server…”
$EXEC $CONF
fi
if [ “$?”=”0” ]
then
echo “Redis is running…”
fi
;;
stop)
if [ ! -f $PIDFILE ]
then
echo “$PIDFILE exists, process is not running.”
else
PID=$(cat $PIDFILE)
echo “Stopping…”
$REDIS_CLI -p $REDISPORT SHUTDOWN
sleep 2
while [ -x $PIDFILE ]
do
echo “Waiting for Redis to shutdown…”
sleep 1
done
echo “Redis stopped”
fi
;;
restart|force-reload)
${0} stop
${0} start
;;
*)
echo “Usage: /etc/init.d/redis {start|stop|restart|force-reload}” >&2
exit 1
esac
```
- 设置脚本权限chmod 755 /etc/init.d/redis
- 设置卡机自启动chkconfig redis on
- 设置/myredis/redis.conf中,bind IP地址为Linux虚拟机IP