一、简介
1、redis是一个开源的、高性能的、基于键值对的缓存和存储系统,通过提供多种键值数据类型适应不同场景下的缓存和存储需求,同时redis高级功能能胜任消息队列、任务队列等不同角色.
2、内存存储与持久化:redis中所有数据都存储在内存中.但有问题,程序退出的时候内存中的数据会丢失,不过redis提供对持久化的支持,即将内存中的数据异步写入到硬盘中,不影响提供服务.
3、redis可以为每个键设置生存时间,到期后会自动删除,这一功能让redis成为了出色的缓存系统.作为缓存系统,redis还可以限定占用的最大内存空间,在数据达到空间限制后可以按照一定规则自动淘汰不需要的键.
二、安装
1、将 redis 安装到/usr/local/webserver/redis
mkdir -p /usr/local/webserver/redis
2、下载安装包
wget http://download.redis.io/redis-stable.tar.gz tar xzf redis-stable.tar.gz cd redis-stable make //报错的话 install gcc
make install //redis可执行文件redis-cli,redis-server等会被复制到/usr/local/bin,命令行直接输入即可执行
最常用的两个程序是:redis-server是redis服务器,启动redis即运行redis-server;redis-cli是reids自带的命令行客户端.
3、启动redis
1>直接运行:$ redis-server
2>通过初始化脚本,这样能在生产环境中,使redis随系统自动运行
(1)配置初始化脚本,将redis源码目录中的utils文件夹中的redis_init_script复制到/etc/init.d,命名为redis_6379,内容不做改动:
#!/bin/sh # # Simple Redis init.d script conceived to work on Linux systems # as it does use of the /proc filesystem. REDISPORT=6379 EXEC=/usr/local/bin/redis-server CLIEXEC=/usr/local/bin/redis-cli PIDFILE=/var/run/redis_${REDISPORT}.pid CONF="/etc/redis/${REDISPORT}.conf" 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 ;; stop) if [ ! -f $PIDFILE ] then echo "$PIDFILE does not exist, process is not running" else PID=$(cat $PIDFILE) echo "Stopping ..." $CLIEXEC -p $REDISPORT shutdown while [ -x /proc/${PID} ] do echo "Waiting for Redis to shutdown ..." sleep 1 done echo "Redis stopped" fi ;; *) echo "Please use start or stop as first argument" ;; esac
(2)建立需要的目录
/etc/redis //存放redis配置文件 /var/redis/端口号 //存放redis持久化文件
(3)将redis.conf复制到/etc/redis,命名为6379.conf,修改如下内容
daemonize yes //使redis能以守护进程模式运行 pidfile /var/run/redis_6379.pid //设置redis PID 文件位置 port 端口号 dir /var/redis/6379 //设置持久化文件存放位置
现在就可以使用/etc/init.d/redis_6379 start 来启动 redis,而后通过如下命令使redis随系统自动启动:
进入/etc/init.d目录,执行命令:chkconfig --add redis_6379,结果报错:redis_6379 服务不支持 chkconfig 解决:编辑redis_6379,第二行添加:#chkconfig: 2345 80 90
4、停止redis : $redis-cli shutdown,kill -9 redis进程的PID也可正常结束redis,redis持久化保证数据不会丢失
ps:多数据库,redis默认支持16个数据库,客户端与redis建立连接后悔自动选择0号数据库,这些数据库与mysql有大区别,不支持命名,不支持设置不同访问密码,且多个数据库之间不是完全隔离的,例:flushall命令会清空所有数据,这些数据库更像一种命名空间,不适宜存储不同应用的数据,不同的应用应使用不同的redis实例存储数据,可以使用0号存储测试数据,1号存储线上数据.
三、入门
1、热身
1>获得符合规则的键名列表(通配符规则 ? * [] x) 例: keys * 键数量多时,影响性能
2>判断一个键是否存在: exists bar
3>删除键:del bar
4>获得键的数据类型:type bar 结果是string、hash、list、set、zset中的一种
2、字符串
1>set、get、incr(当存储的字符串是整数时,当incr的键不存在时,默认值是0)
2>实践:文章访问量统计(伪代码)
#获得文章ID &postID = incr posts:count; #将文章的诸多元素序列化成字符串 $serialzedPost = serialize($tilte,$content,$author,$time); #序列后的字符串存入一个字符串类型的键中 set posts:postID:data,$serialzedPost #获取文章数据(以id为42文章为例) $serialzedPost = get posts:42:data; $tilte,$content,$author,$time = unserialize($serialzedPost); #获取并递增文章的访问数量 $count = incr post:42:page.view
3>命令拾遗
增加指定整数:incrby bar 2,减少整数:decr/decrby,增加指定浮点数:incrbyfloat bar 2.7,尾部追加:append key " world",获取字符串长度:strlen key,同时获取/设置多个键值:mset k1 v1 k2 v2,mget k1 k2,位操作 略.
3、散列类型
提问:想获取文章的标题,必须把整个文章数据取出来后反序列化,在传输和处理时会造成资源浪费?
1>redis是采用字典结构键值对形式存储数据,散列类型也是一种字典结构,但字段值只能是字符串,也就是说散列类型不能嵌套其它的数据类型,redis其它数据结构同样如此.
2>散列类型适合存储对象,例:存储id为2的汽车对象: hset car:2 color "白色",hset car:2 name "奥迪",hset car:2 price "90万"
3>赋值和取值:hset,hget car:2 name, hmset car:2 color "白色" name "奥迪",hmget car:2 name price, hgetall car:2,hexists car:2 name,hsetnx car:2 name "大众"(字段不存在时赋值,注意字段和键含义的不同,car:2是键,name是字段),hincrby pserson score 60,hdel car:2 price(删除字段,删除car:2要用del)
4>实践:将字符串实践改为散列实践 略.(还有一种方式是组合使用多个字符串类型来存储一片文章,但是散列更直观,更易维护(可用散列的命令),节省空间)
5>命令拾遗:hkeys key,hvals key,hlen key
4、列表类型
1>列表类型内部是使用双向列表实现的,向两端添加元素的时间复杂度是O(1),即使列表中元素很多,通过索引访问中间元素比较慢,两端元素快,这种特性能快速完成关系数据库难以应付的场景:社交网站的新鲜事,我们只关心最新的内容.
2>借助列表类型,redis还可以作为队列使用.
3>命令:lpush key value(头),rpush key value(尾),lpush num 2 3(同时添加多个元素),lpop key,rpop key(两端弹出),llen key (O(1)级别),lrange key start stop(获取列表片段),lrem key count value(删除列表中指定的值)
3>实践:存储文章ID列表,使用lrange来实现文章分页显示 (伪代码)
$postsPerPage=10 $start = $(currnetPage -1) * $postsPerPage $end = $currnetPage * $postsPerPage-1 $postsID = lrange posts:list,$start,$end #循环方式读取文章 for each $id in $postsID $post = hgetall post:$id print 文章标题: $post.title
ps:有两个问题,文章发布时间不宜修改,改后要从新排序list;文章数量多时访问中间页面性能较差.
4>命令拾遗
获得/设置指定索引的元素值:lindex key index,list key index value,只保留列表指定片段:ltrim key start end.
5、集合类型
1>集合无序唯一,列表有序不唯一,集合常见操作是加入,删除,判断是否存在,redis内部使用hash table实现,这些操作时间复杂度都是O(1),多个集合可以进行并集,交集,差集.
2>命令:sadd letters a,sadd letters a b c(添加一个或多个元素),srem letters c d (删除一个或多个元素),smembers letters (返回集合中所有元素),smembers letters a(判断元素是否在集合中)
3>集合间运算:sadd setA 1 2 3,sadd setB 2 3 4,sdiff setA setB(差集),sinter setA setB(交集),sunion setA setB(并集)
4>实践:存储文章标签 (伪代码)
#给id为42的文章添加标签 sadd post:42:tags,闲言碎语, 技术,java #删除标签 srem post:42:tags 闲言碎语 #显示所有标签 $tags = smembers post:42:tags print $tags
5>命令拾遗
获取集合中元素个数:scard letters,进行集合运算并将结果存储:sdiffstore,sinterstore,sunionstore,随机获得集合中的元素:srandmember letters,随机弹出一个元素:spop letters.
6、有序集合类型
1>在集合的基础上有序集合为集合中的每个元素都关联了一个分数,这使的我们不仅可以完成插入、删除、判断是否存在等集合类型支持的操作还能获得分数最高(低)的前N个元素,等于分数有关的操作,集合中的元素不同,分数可以相同.
2>有序集合和列表的区别:1.二者都有序,都可获取某一范围的元素.2.列表获取靠近两端的数据速度极快,中间慢,有序集合读取中间的也很快.3.列表不能调整元素位置,有序集合可以,通过调整元素的分数.4.有序集合更耗内存.
3>命令:添加元素:zadd scoreboard 89 tom 67 peter 100 david,获得元素的分数:zscore scoreboard tom,获得排名在某个范围的元素列表:zrange scoreboard 0 2(按照分数从小到大的顺序返回索引为0到2之间的元素,索引从0开始,-1代表最后一个元素,如果元素分数相同按字典顺序来排序),获得指点分数范围的元素:zrangebyscore scoreboard 80 100,可以在尾部添加limit语句,增加某个元素的分数:zincrby scoreboard 4 tom(-4代表减).
4>实践
1、按点击量排序 (伪代码)
要按照文章的点击量排序,就必须再额外使用一个有序集合类型的键来实现,在这个键中以文章的ID作为元素,文章的点击量作为该元素的分数,键命名为post:page.view,每次用户访问一篇文章,就通过zincrby posts:page.view 1 文章ID,来跟新访问量.
$postsPerPage=10 $start = $(currnetPage -1) * $postsPerPage $end = $currnetPage * $postsPerPage-1 $postsID = zrange posts:list,$start,$end #循环方式读取文章 for each $id in $postsID $post = hgetall post:$id print 文章标题: $post.title
之前使用字符串类型键post:文章ID:page.view来记录文章的访问量,现在已经不需要了,获取文章的访问量通过zscore posts:page.view 文章ID.
2、改进按时间排序
之前每次发布新文章时都将文章ID加入到名为posts:list的列表类型键中来获得按照时间顺序排列的文章列表,但是由于列表类型更改元素的顺序比较麻烦,而如今不少的博客都支持更改文章的发布时间,可以采用有序集合来替代列表类型,自然元素任然是文章ID,元素分数是文章发布的UNix时间,通过修改元素对应的分数来更改时间的目的.
5>命令拾遗
获得集合中元素个数:zcard scoreboard,获得指定分数范围的元素个数:zcount scoreboard 90 100,删除一个或多个元素:zrem scoreboard Wendy,按分数范围删除元素:zremrangebyscore testRem (4 5,获得元素的排名:zrank scoreboard Peter,计算有序集合的交集 略.
四、进阶
1、事务
1>引出:使用redis来存储微博中的用户关系,思路是对每个用户使用两个集合类型键,分别名为user:用户ID:followers和user:用户ID:following.
def follow($currentUser,$targetUser)
sadd user:$currentUser:following $targetUser
sadd user:$targetUser:followers $currentUser
如果ID为1用户关注ID为2用户,只需要follow(1,2),这里如果第一条命令执行完后由于某种原因第二条命令没有执行,就会出问题,可用事务解决.
2>概述
redis中的事务是一组命令的集合,这组命令要么都执行,要么都不执行,上例开启事务如下:
redis>multi OK redis>sadd "user:1:following" 2 QUEUED redis>sadd "user:2:followers" 1 QUEUED exec 1)(integer) 1 2)(integer) 1
multi告诉redis要开启事务,QUEUED表示命令进入等待执行的事务队列,EXEC告诉redis将等待执行的事务队列中的所有命令按照发送顺序依次执行,EXEC命令的返回值就是这些命令的返回值组成的列表,如果在EXEC命令前客户端断线了,则redis会清空事务队列,除此之外redis的事务还能保证一个事务内的命令依次执行而不被其他命令插入(感觉像是插入没用,exec的时候会把插入命令的执行结果覆盖).
3>错误处理
如果事务的某个命令执行出错,redis会如何处理?
(1)语法错误:命令不存在或命令参数不对,则所有命令都不会执行.
(2)运行错误:比如使用散列类型命令操作集合类型键,这种在执行之前redis无法发现,这种如果事务里的一条命令运行错误,事务里的其它命令依然会继续执行.redis的事务没有回滚功能,如果出错,必须手动处理.
这两种错误仔细的话完全可以避免.
2、watch命令
1>引出,我们借助get和set两个命令自己实现incr函数,伪代码如下:
def incr($key) $value = GET $key -->这里来两个客户端取到相同的值 $value = $value + 1 SET $key, $value -->这里会出现覆盖现象
上述代码如果只有一个客户端,那么没问题,但是当多个客户端连接到redis时,则有可能出现竞态条件.为了解决这个问题,需要在get获得键值后保证该键值不被其它客户端修改,直到函数执行完成后才允许其它客户端修改该键值,这样能防止竞态条件,要实现这个思路需要事务家族的另一位成员:watch,watch命令可以监控一个或多个键,一旦其中有一个键被修改,之后的事务就不会执行,监控一直持续到EXEC命令.
def incr($key) WATCH $key $value = GET $key $value = $value + 1 MULTI SET $key $value EXEC
2>演示watch效果,session1和session2代表两个客户端:
Session 1 (1)第1步 redis 127.0.0.1:6379> get age "10" redis 127.0.0.1:6379> watch age OK redis 127.0.0.1:6379> multi OK Session 2 (2)第2步 redis 127.0.0.1:6379> set age 30 OK redis 127.0.0.1:6379> get age "30" Session 1 (3)第3步 redis 127.0.0.1:6379> set age 20 QUEUED redis 127.0.0.1:6379> exec (nil) redis 127.0.0.1:6379> get age "30"
3>实现电商秒杀抢购功能
<?php header("content-type:text/html;charset=utf-8"); $redis = new redis(); $result = $redis->connect('10.10.10.119', 6379); $mywatchkey = $redis->get("mywatchkey"); $rob_total = 100; //抢购数量 if($mywatchkey<$rob_total){ $redis->watch("mywatchkey"); $redis->multi(); //设置延迟,方便测试效果. sleep(5); //插入抢购数据 $redis->hSet("mywatchlist","user_id_".mt_rand(1, 9999),time()); $redis->set("mywatchkey",$mywatchkey+1); $rob_result = $redis->exec(); if($rob_result){ $mywatchlist = $redis->hGetAll("mywatchlist"); echo "抢购成功!<br/>"; echo "剩余数量:".($rob_total-$mywatchkey-1)."<br/>"; echo "用户列表:<pre>"; var_dump($mywatchlist); }else{ echo "手气不好,再抢购!";exit; } } ?>
3、过期时间
1>expire命令介绍
实际开发中经常遇到一些有效的数据,比如优惠活动、缓存、验证码等,过了一定的时间就需要删除这些数据,在关系型数据库中需要一个额外的字段记录到期时间,而在redis中可以使用expire设置一个键的过期时间,使用如下:
#expire key seconds set session:29e3d uid1314 expire session:29e3d 900 //返回1成功,0键不存在或设置失败
如果想知道还有多久过期,使用ttl命令:ttl session:29e3d,如果想取消过期时间限制persist session:29e3d.其它对键进行操作的命令(incr,lpush,hset等)均不会影响键的过期时间.
2>实现访问频率限制之一
为了减轻服务器压力,需要限制每个IP一段时间的最大访问量,与时间有关的操作都要联想到expire命令,思路:对每个IP使用一个名为rate.limiting:用户IP的字符串类型键,每次访问使用INCR命令递增该键的键值,如果递增的值是1还要设置该键的过期时间是1分钟,这样每次用户访问都要读取该键的值,如果超过了100就提示用户稍后访问,上述流程伪代码:
$isKeyExists = EXISTS rate.limiting:$IP if $isKeyExists = 1 $times = incr rate.limiting:$IP; if($times > 100) pring "访问频率超过限制" exit else incr rate.limiting:$IP; expire rate.limiting:$IP 60
3>实现访问频率限制之二
上例中代码任然有问题,如果一个用户在一分钟的第一秒访问了一次,在一分钟的最后一秒访问了9次,又在下一分钟的第一秒访问了9次,那么是可以通过现在的访问频率限制的,在一些场合中还是需要粒度更小的控制方案,如果要精确的保证每分钟最多只能访问10次,可以使用列表类型的键来记录下用户最近访问10次的时间,一旦键中的元素个数超过10个就判断最早的时间和现在的时间是否小于1分钟,如果是则将现在的时间加入到列表中,如果不是则将时间加入的同时要把最早的时间删除.
$listLength = llen rate.limiting:$IP if($listLength < 10) lpush rate.limiting:$IP now() else $times = lindex rate.limiting:$IP -1 if(now() - $times < 60) print "访问频率超过限制" else lpush rate.limiting:$IP now() ltrim rate.limiting:$IP,0,9
此方法会占用较多的存储空间,实际使用时还需要开发者自己权衡,另外该方法也会出现竞态条件,代码中略了.
面试题:登录时如果1分钟连续输入3次则要输入验证码,如果5分钟连续输错3次则冻结账户?
//登录时1分钟连续输入3次则弹出验证码 $listLength = llen rate.limiting:$uID if($listLength < 2) lpush rate.limiting:$uID now() else $times = lindex rate.limiting:$IP -1 if(now() - $times < 60) print "弹出验证码" else lpush rate.limiting:$uID now() ltrim rate.limiting:$IP,0,2 //5分钟连续输错3次则冻结账户 $listLength = llen rate.limiting:$uID if($listLength < 2) lpush rate.limiting:$uID now() else $times = lindex rate.limiting:$IP -1 if(now() - $times < 60*3) print "冻结账户" else lpush rate.limiting:$uID now() ltrim rate.limiting:$IP,0,2
4>实现缓存
为了提高网站的负载能力,常需要将一些访问频率较高但对cpu或io资源消耗较大的操作结果缓存起来,并希望这些缓存过一段时间自动过期.如下案例,教务网站对学生成绩排名,由于学生成绩总在变化,需要每隔两个小时重新计算一次排名,这可以通过expire来实现,如下:
$rank = get cache:rank if not $rank $rank = 计算排名... multi set cache:rank $rank expire cache:rank 7200 exec
然而在一些场合这种方法并不能满足需求,实际开发中,可以设置redis能使用的最大内存,让redis按照一定的规则淘汰不需要的缓存键,这种方式在redis用作缓存系统时非常实用.具体的设置方法是:修改配置文件的maxmemory参数,当超过了这个限制时,redis会依据maxmemory-policy参数指定的策略(算法)来删除不需要的键,有一个算法LRU,即最近最少使用.
4、排序
1>redis提供了sort命令来对列表类型、集合类型和有序集合类型键进行排序.
//对列表排序 127.0.0.1:6379> lpush ww 4 1 3 (integer) 3 127.0.0.1:6379> sort ww 1) "1" 2) "3" 3) "4" //对集合排序 127.0.0.1:6379> sadd wx 9 2 7 (integer) 3 127.0.0.1:6379> sort wx 1) "2" 2) "7" 3) "9" //对有序集合排序,会忽略分数 127.0.0.1:6379> zadd wwww 10 2 3 1 7 3 (integer) 3 127.0.0.1:6379> sort wwww 1) "1" 2) "2" 3) "3" //列表、集合、有序集合中可以通过alpha实现按照字典顺序排序非数字元素 127.0.0.1:6379> zadd w 10 z 3 x 7 (error) ERR syntax error 127.0.0.1:6379> sort www alpha 1) "j" 2) "x" 3) "z" //元素按从大到小排列 127.0.0.1:6379> sort ww desc 1) "4" 2) "3" 3) "1" //limit返回指定范围的结果 127.0.0.1:6379> sort ww desc limit 1 1 1) "3"
2>by参数--by参考键
很多情况下列表,集合,有序集合中存储的元素值代表的是对象的ID,单纯对这些ID自身排序意义不大,更多时候我们希望根据ID对应的对象的某个属性进行排序,如之前的使用散列类型键来存储文章对象,其中time字段存储的文章的发布时间,ID为2,6,12,26的四篇文章time字段分别为3423,23,2,45(Unix时间),按照发布时间排序,使用sort命令的by参数:
//如下sort将不再依据元素自身的值进行排序,而是对每个元素的值替换参考键中的第一个*并获取其值,然后依据该值进行排序 sort tag:posts by post:*->time desc
get参数、store参数 略
5、消息通知
1>引出:小白博客中要加入邮件订阅功能,这样当小白发布新文章后订阅小白博客的用户就可以收到通知邮件了,实现很简单就是博客首页放一个文本框供访客输入自己的邮箱,提交后博客会将该邮箱存入redis的一个集合类型键中,每当发布新文章时,就向邮箱地址发送通知邮件,可是有个问题就是输入邮箱后,页面需要很长时间才能载入完,因为为了确保用户没有输入他人的邮箱,在提交之后程序会向用户输入的邮箱发送一封包含确认连接的邮件,只有用户点击这个链接后对应的邮箱地址才会被程序记录.可是发送邮件需要连接到一个远程的邮箱服务器,网络好的情况也需要2秒,所有每次用户提交邮箱后页面都要等待程序发送邮件才能加载出来,而加载出来的页面上显示的内容只是提示用户查看自己的邮箱单机确认链接.完全可以等页面加载出来后再发送邮件,这样用户就不用等了.
2>任务队列
小白的问题在网站开发中十分常见,当页面需要进行如发送邮件、复杂数据运算等耗时的操作时会阻塞页面的渲染,为了避免用户等太久,应该使用独立的线程来完成这类操作,不过一些编程语言和框架不易实现多线程,就容易想到通过其他进程来实现,就这个例子来说,设想有一个进程能完成发送邮件的功能,那么在页面中只需要想办法通知这个进程向指定的地址发送邮件就可以了,通知的过程可以借助队列来实现,与任务队列进行交互的实体有两类,一是生产者一是消费者(生产者消费者无需知道彼此实现细节,只需要约定好任务的描述格式,生产者消费者可以有不同的团队使用不同的编程语言编写).当需要发送邮件时,页面程序会将收件地址、邮件主题和邮件正文组成一个任务存入任务队列.同时发邮件的进程会不断检查任务队列,一旦发现有新的任务便会将其中队列中取出并执行.由此实现进程间的通信.
3>redis实现任务队列
上例中,完成邮件任务需要知道收件地址,邮件主题,邮件正文,所以生产者需要将这3个信息组成对象并序列化成字符串,然后将其加入任务队列中,而消费者循环从队列中拉取任务,伪代码如下:
loop #如果任务队列中没有新任务,brpop命令会一直阻塞,不会执行execute() #brpop接收两个参数,键名和超时时间,单位秒,如果超过了时间没有新元素就返回nil,0表示无限制等待 $task = brpop queue 0 #返回值是一个数组,分别是键名和元素值 execute($task[1])
4>优先级队列
如果发送确认邮件和和发送通知邮件使用同一个消费者的时候,用户收到确认邮件可能等很长时间,体验差,这里要使用优先队列.brpop可以同时接收多个键,意义是同时监测多个键,如果所有键都没有元素则阻塞,如果其中有一个键有元素则会从该键中弹出元素,如果多个键都有元素则按照从左到右的顺序取第一个键中的一个元素,借此特性可以实现区分优先级的任务队列,例:
127.0.0.1:6379> lpush queue:2 task1 (integer) 1 127.0.0.1:6379> lpush queue:3 task2 (integer) 1 127.0.0.1:6379> brpop queue:1 queue:3 queue:2 0 1) "queue:3" 2) "task2"
我们分别使用queue:confirmation.email和queue:notification.email两个键存储发送确认邮件和发送通知邮件的两种任务,伪代码如下:
loop $task = brpop queue:confirmation.email queue:notification.email 0 execute($task[1])
发布/订阅模式、管道、节省空间 略.
五、实践、脚本(lua与redis) 略
六、持久化
我们希望redis能将数据从内存中以某种形式同步到硬盘中,使得重启后可以根据硬盘中的记录恢复数据.这一个过程就是持久化.redis支持两种持久化方式RDB和AOF,前者会根据指定的规则定时将内存中的数据存储到硬盘上,后者在每次执行命令后会将命令本身记录下来,可以使用其中一种或多种,更多情况是二者结合使用.
1、RDB方式
RDB方式是通过快照完成的,当符合一定条件时redis会自动将内存中的所有数据生成一份副本并存储到硬盘上,这个过程就是快照,redis会在以下几种情况对数据进行快照:根据配置规则进行快照;用户执行save或bgsave;执行flushall命令;执行复制(replication)时.
1>根据配置规则进行快照: redis配置文件中每条快照条件占一行,条件之间是或关系,如下
save 900 1 //900秒内1个键被更改 save 300 10 save 60 10000
2>当进行服务重启、手动迁移以及备份时需要手动快照,save命令不推荐会阻塞客户端请求,bgsave推荐,不阻塞客户端,flushall略.
3>当设置了主从模式时,redis会在复制初始化时进行自动快照,即使没有定义自动快照条件,并且没有手动执行快照操作,也会产生RDB快照文件.
2、快照原理
redis使用fork函数复制一份当前进程的副本也就是子进程,父进程继续接受并处理客户端发来的命令,子进程开始将内存中的数据写入硬盘的临时文件,子进程写入完所有的数据后用该临时文件替换旧的RDB文件,至此一次快照操作完成.这个过程中,快照结束后才会将旧的文件替换成新的,也就是说RDB文件任何时候都是完整的,这使的我们可以通过定时备份RDB文件来实现redis的数据库备份.redis在启动的时候会读取RBD快照文件,将数据从硬盘载入到内存.通过RDB方式实现持久化,一旦redis异常退出,就会丢失最后一次快照以后更改的数据,这就需要开发者根据具体的应用场合,通过组合设置自动快照条件的方式来将可能发生的数据损失控制在能接受的范围,例如,使用reids作为缓存,丢失最近几秒的数据或者最近更新的几十个键并不会影响太大.如果数据相对重要,希望将损失将到最小,则可以使用AOF方式持久化.
3、AOF方式
1>当使用redis存储非临时数据时,一般需要打开AOF持久化来降低进程终止导致的数据丢失,AOF会将redis执行的每一条命令追加到硬盘中,这一过程显然会降低redis的性能,但可以接受.默认情况redis没有开启AOF,通过配置appendonly yes来开启.
2>同步硬盘数据:通过配置appendfsync everysec(每秒执行一次),appendfsync always(每次执行都同步,最安全也最慢),appendfsync no(不主动同步),将硬盘缓存内容主动同步到硬盘,之前的每条命令只是写到了缓存中.
redis允许同时开启AOF和RDB,既保证了数据安全又使得进行备份等操作十分容易,此时重新启动redis会使用AOF文件来恢复数据,因为AOF方式的持久化可能丢失的数据更少.
七、集群
从结构上,单个redis会发生单点故障,同时一台服务器要承受所有的请求负载,这就需要为数据生成多个副本并分配在不同的服务器上.从容量上,单个redis的内存容易成为存储瓶颈,所以需要进行数据分片.同时拥有多个redis服务器会面临如何管理集群的问题,包括增加节点、故障恢复等操作.下面依次介绍redis中的复制、哨兵和集群.
1、复制
1>为了避免单点故障,通常的做法是将数据库复制多个副本部署到不同的服务器上,这样即使一台出现故障,其它服务器依然可以提供服务.为此,redis提供了复制功能,可以实现当一台数据库中的数据更新后,自动将更新的数据同步到其它数据库上.复制中数据库分主和从,主进行读写操作,当写操作导致数据变化时会自动将数据同步给从数据库,而从数据库一般是只读的(可设置为写,但不应该),并接收主同步过来的数据,一个主可以有多个从,而一个从只能有一个主.
2>redis中使用复制,只要在从数据库配置文件中加入"slaveof 主数据库地址 主数据库端口"即可,主数据库无需任何配置.
3>原理:当一个从库启动后,会向主库发送SYNC命令.主库接收到SYNC后会开始在后台保存快照,并将保存快照期间的命令缓存起来,快照完成后,主会将快照文件和缓存的命令发送给从库,从库收到后会载入快照文件并执行收到的缓存命令,以上过程称为复制初始化,复制初始化结束后,主库每当收到写命令时就会将命令同步给从库(这一过程称复制同步),从而保证主从库的数据一致.
ps:乐观复制 redis采用了乐观复制的策略,容忍一定时间内主从数据库的内容不同,但是两者的数据会最终同步,具体来说,redis在主从数据库之间复制数据的过程本身是异步的,这意味着,主数据库执行完客户端请求命令后会立即将命令在主数据库的执行结果返回给客户端,并异步地将命令同步给从数据库,而不会等待从数据库接收到该命令后再返回给客户端,这一特性保证了启用复制后主数据库的性能不会受影响,但另一方面也会产生一个主从数据不一致的时间窗口.当主库数据变动了,在同步到从库之前,如果两个数据库之间的网络连接断开了,此时二者之间的数据就会不一致,redis提供了两个配置选项来限制只有当数据同步给指定的从库时,主库才是可写的:min-slaves-to-write 3(至少同步3个从,主才可写),min-slaves-max-lag 10(允许从最长失去连接的时间),这一特性默认是关闭的.
4>读写分离(redis需要读写分离吗?http://www.zhihu.com/question/38768751):通过复制可以实现读写分离,以提高服务器的负载能力(就目前jedis版本中的JedisSentinelPool做不到这点),在常见的电商网站中,读的频率大于写,当单机的redis无法应付大量的读请求时,可以通过复制建立多个从库节点,主库只负责写操作,而从负责读操作.而当单个主不能满足需求时,就要使用redis 3.0推出的集群功能.
2、哨兵
在一个典型的一主多从的redis系统中,从数据库在整个系统中起到了数据冗余备份和读写分离的作用,当主数据遇到异常中断服务后,开发者可以手动将一个从提升为主,已使系统继续提供服务,整个过程相对麻烦且需要人工介入,难以实现自动化.使用redis 2.8提供的哨兵2,哨兵1问题较多.
1、哨兵的作用是监控redis系统的运行状况,它的功能包括以下两个:监控主库和从库是否正常运行,主库出现故障时自动将从库转换为主库.
2、使用:建立一主两从的架构,接下来配置哨兵,建立一个配置文件:sentinel.conf,内容为:sentinel monitor mymaster 127.0.0.1 6379 1,其中mymaster是监控的主数据名字(自定义),后两个参数表示主库地址和端口,最后1表示最低通过票数,接下来执行哨兵进程 $ redis-sentinel /path/to/sentinel.conf,哨兵只需要配置主库即可,会自动发现从库.
3>原理:哨兵和主数据库连接建立完成后,哨兵会定时执行下面三个操作.
(1)每10秒哨兵会向主库和从库发送info命令.
(2)每2秒哨兵会向主库和从库的_sentinel_:hello频道发送自己的信息.
(3)每1秒哨兵会向主库和从库和其他哨兵节点发送PING命令.
首先发送info命令获取当前数据库的相关信息,比如新增的从数据库的建立连接并加入列表,对主数据库角色变化(故障恢复操作引起的)进行信息更新等.实现了自动发现从数据库和其他哨兵节点后,哨兵要做的就是定时监控这些数据库和节点有没有停止服务,通过向这些节点发送PING命令.当超过down-after-milliseconds选项指定时间后,如果被PING的数据库或节点仍未回复,则哨兵认为其主观下线.接下来该哨兵会询问其他哨兵节点也认为其主观下线,当达到指定的数量,也就是之前的1数字代表的含义时,哨兵会认为其客观下线,并选举领头的哨兵节点对主从系统发起故障恢复(为了保证只有一个哨兵来进行故障恢复),Raft算法选举,选举出来后,领头哨兵节点会挑选一个从库来充当主库,也有算法,略,选出一个从库后,领头哨兵将会向从库发送slaveof no one 命令使其升格为主库,向其他从库发送slaveof命令使其成为新主库的从数据库,最后一步则是更新内部记录,将已经停止服务的就的主库更新为新的主库的从库,使得当其恢复服务时自动以从库的身份继续服务.
4、哨兵的部署,当只有一个哨兵时,哨兵本身就可能会发生单点故障,相对稳妥的方案是:
1>为每个节点无论主库还是从库都部署一个哨兵(当节点过多时哨兵也过多难免影响性能,根据实际情况选择).
2>使每个哨兵与其他节点的网络环境相同或相近.
ps: Jedis的JedisSentinelPool源代码分析:https://segmentfault.com/a/1190000002690506
3、集群 如果单个主库不能满足要求的话就要使用集群来部署多主 略.