前言
前面我们已经学习了redis的数据类型,接下来将简单学习下redis的事务,排序,管道,优化存储空间以及管理等知识。
事务
事务的概念在此不赘述,学过数据库原理的都应该知道。
redis的事务:先将属于一个事务的命令发送给redis,然后再让redis依次执行这些命令:
MULTI //开始一个事务
//事务的命令
EXEC //执行事务
EXEC告诉redis将等待执行的事务队列中的所有命令(即刚才所有返回QUEUED的命令)按照发送顺序依次执行。
错误处理
1.语法错误:命令不存在或者命令参数的个数不对。
只要有一个命令有语法错误,执行EXEC后redis就会直接返回错误,那些语法正确的命令也不会执行。
2.运行错误:在命令执行时出现的错误,比如使用散列类型的命令操作 集合类型的键。
如果一条命令出现了运行错误,事务里其他的命令依然会继续执行。
- redis的事务没有回滚功能。
WATCH命令
事务中的每个命令的执行结果都是最后一起返回的,所以我们无法将前一条命令的结果作为下一条命令的参数。
下面举个例子,假如我们要自己实现INCR命令,可以根据GET命令的返回值来确定该键是否已经存在,然后进行对应得SET操作。为防止竞态,可以使用事务,但是事务存在上面提到的问题,因此无法解决竞态问题。
我们再看看WATCH命令:WATCH命令可以监控一个或者多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令被执行。
例子:
> SET key 1
OK
> WATCH key
OK
> SET key 2
OK
> MULTI
OK
> SET key 3
QUEUED
> EXEC
(nil)
> GET key
"2"
可以看到,我们在指向WATCH命令后,事务执行前修改了key的值,因此最后事务并没有执行,EXEC返回空结果。
因此使用WATCH就可以通过事务实现自己的incr函数:
def incr($key)
WATCH $key
$value = GET $key
if not $value
$value = 0
$value = $value + 1
MULTI
SET $key, $value
result = EXEC
return result[0]
需要注意的三点:
1.EXEC返回值是多行字符串类型
2.WATCH命令的作用只是当监控的键值被修改后阻止之后一个事务的执行,而不能保证其他客户端不修改这一键值。因此,如果EXEC执行失败,我们需要重新执行整个函数。
3.EXEC执行之后会取消对所有键的监控,我们也可以使用UNWATCH来取消监控。
生存时间
在redis中,可以使用EXPIRE命令设置一个键的生存时间,单位是秒:
> SET foo bar
OK
> EXPIRE foo 20
(integer) 1
> TTL foo
(integer) 15
> TTL foo
(integer) 7
> TTL foo
(integer) -1
可以看到,随着时间不同,foo键的生存时间逐渐减少,20秒后foo会被删除,当键不存在时TTL返回-1,另外,当没有设置生存时间时,默认永久存在,TTL同样返回-1。
使用PERSIST命令可以取消键的生存时间:
PERSIST foo
此外使用SET或GETSET命令为键赋值也会同时清除键的生存时间。
EXPIRE的参数必须是整数,如果想更精确,可以使用PEXPIRE,其单位是毫秒。
- 如果使用WATCH监控一个有生存时间的键,该键时间到期自动删除并不会被WATCH命令认为该键被改变。
缓存
redis可以作为缓存使用,一方面我们可以通过给键设置生存时间来定期删除缓存,另一方面,如果内存已经达到上限,redis将按照一定规则淘汰不需要的缓存键(常见的是LRU-最近最少使用算法)。
最大可用内存大小(单位是字节):配置maxmemory;
淘汰策略:配置maxmemory-policy。
排序
1.使用有序集合
2.使用SORT命令(有BY, GET, STORE等参数可以选择)
时间复杂度为O(n+mlogm),n表示要排序的列表中元素个数,m表示要返回的元素个数。
消息通知
消息通知可以使用“任务队列”——“传递任务的队列”,也就是常见的生产者/消费者模型,生产者会将需要处理的任务放入任务队列中,而消费者则不断地从任务队列中读入任务信息并执行。
优点:
- 松耦合:生产者和消费者无需知道彼此的实现细节;
- 易于扩展:消费者可以有多个,且可以分布在不同的服务器中。
使用redis实现任务队列
生产者:将任务使用LPUSH命令假如到某个键中;
消费者:不断地适应RPOP命令从该键中取出任务。
以下为消费者的伪代码:
# 无限循环读取任务队列中的内容
loop
$task = RPOP queue
if $task
execute($task)
else
wait 1 second
以上不断轮询,效率低下,我们可以使用BRPOP来优化:
loop
#如果队列中没有新任务,BRPOP将会一直阻塞
$task = BRPOP queue, 0
#返回值是一个数组,数组第二个元素是我们需要的任务
execute($task[1])
BRPOP接收两个参数:键名和超时时间(秒为单位,0表示无限等待)。
BRPOP返回两个值:键名和元素值。
优先队列
有下面的情况:假设你的网站有推送功能,即当你发表新文章时,会发邮件提醒关注者,同时,你的网站注册时需要邮件验证。如果没有使用优先队列,那么当你发表新文章之后,发邮箱的线程被占用,使得注册用户得不到及时的响应。因此需要使得注册的邮件优先被处理发送。
loop
$task =
BRPOP queue:confirmation.email,
queue:notification.eamil,
0
execute($task[1])
BRPOP可以同时接收多个键,如果多个键都有元素则按从左到右的顺序取第一个键中的一个元素。
“发布/订阅”模式
除了实现任务队列之外,redis还提供了一组命令可以让开发者实现“发布/订阅”(publish/subscribe)模式:
订阅者可以订阅一个或若干个频道(channel),而发布者可以向指定的频道发送消息,所以订阅此频道的订阅者都会收到此消息。
发布消息:
PUBLISH channel message
订阅消息:
SUBSCRIBE channel [channel ...]
执行SUBSCRIBE命令后客户端会进入订阅状态,不能使用除SUBSCRIBE/UNSUBSCRIBE/PSUBSCRIBE/PUNSUBSCRIBE这4个属于“发布/订阅”模式的命令之外的命令,否则会报错。
PSUBSCRIBE命令可以订阅指定的规则,如:
>PSUBSCRIBE channel.?*
它可以匹配channel.1和channel.10等,不会匹配channel.。
管道
Redis是一个TCP服务器,并支持请求/响应协议。redis的一个请求完成需要下面的步骤:
- 客户端发送一个查询到服务器,并等待服务器的响应。
- 服务器处理命令并将响应返回给客户端。
而通过管道,客户端可以发送多个请求给服务器,而无需等待答复。即通过管道可以一次性发送多条命令并在执行完之后一次性将结果返回。
节省空间
1.精简键名和键值
2.内部编码优化
查看内部编码:
OBJECT ENCODING foo
redis的每个键值都使用一个redisObject结构体保存:
typedef struct redisObject {
unsigned type:4;
unsigned notused:2; /*Not used*/
unsigned encoding:4;
ussigned lru:22; /*lru time*/
int refcount;
void *ptr;
} robj;
type字段是键值的数据类型:
#define REDIS_STRING 0
#define REDIS_LIST 1
#define REDIS_SET 2
#define REDIS_ZSET 3
#define REDIS_HASH 4
encoding字段表示键值的内部编码方式:
//字符串类型编码
#define REDIS_ENCODING_RAW 0
//字符串类型编码
#define REDIS_ENCODING_INT 1
//散列类型,集合类型编码
#define REDIS_ENCODING_HT 2
#define REDIS_ENCODING_ZIPMAP 3
//列表类型编码
#define REDIS_ENCODINGLINKEDLIST 4
//散列类型,列表类型,有序集合类型编码
#define REDIS_ENCODING_ZIPLIST 5
//集合类型编码
#define REDIS_ENCODING_INTSET 6
//有序集合类型编码
#define REDIS_ENCODING_SKIPLIST 7
具体各个类型的优化参见《Redis入门指南》(李子骅 编著)。
脚本
Redis使用Lua解释器用于计算脚本。它Redis从2.6.0版本开始内置,使用脚本用eval命令。
> EVAL script numkeys key [key ...] arg [arg ...]
脚本功能允许开发者使用Lua语言编写脚本传到Redis中执行,在Lua脚本中可以调用大部分的Redis命令。
使用脚本的好处:
- 减少网络开销:原本需要多次请求的代码,使用脚本功能只需要一次请求。
- 原子操作:redis会将整个脚本作为一个整体执行。
- 复用:客户端发送的脚本会永久存储在redis中。
管理
持久化
redis的数据是存储在内存中的,为了使redis在重启之后仍能保证数据不丢失,需要将数据从内存中以某种形式同步到硬盘中,这个过程叫做持久化。
redis支持两种持久化方式:RDB方式和AOF方式。
RDB方式
RDB方式是通过快照(snapshotting)完成的,当符合一定条件时redis会自动将内存中所有数据进行快照并存储到硬盘上。
进行快照的条件可由用户在配置文件中自定义,由两个参数构成:时间(单位为秒)和改动的键的个数。
当指定时间内被更改的键的个数大于指定的数值时就会进行快照。
RDB是默认的持久化方式,配置文件预置了3个条件:
save 900 1
save 300 10
save 60 10000
多个快照条件之间是“或”的关系。想禁止快照只需要把所有的save参数删除即可。
RDB持久化默认生成的文件名为dump.rdb,可以通过配置dir和dbfilename来指定存储的路径和文件名。
快照过程如下:
1.redis使用fork复制一份当前进程的副本;
2.父进程继续接收并处理客户端发来的命令,而子进程开始将内存中的数据写入硬盘中的临时文件;
3.当子进程写入完所有数据后会用该临时文件替换旧的RDB文件。
可以看到,通过临时文件,rdis保证了任何时候RDB文件都是完整的。
RDB文件是经过压缩的二进制格式,占用空间小。
除了自动快照,还可以手动发送SAVE或BGSAVE让redis执行快照,前者在主进程中进行快照,后者会fork子进程进行快照。
另外,你可能会好奇,服务器是怎么知道我做了多少修改的?
服务器中有个dirty计数器和一个lastsave时间戳。
当服务器执行一个数据库修改命令之后,dirty计数器就会进行更新,命令修改了多少次数据库,dirty就会增加多少,如:【set msg hello】修改了一个,那么dirty就加一,如果【mset msg word name nihao age 20】那么dirty就增加三。
lastsave属性记录上次服务器执行保存操作的时间,是一个unix时间戳,通过这两个属性,可以很简单的距离上次保存已经多少时间了,以及修改了多少次数据库,一旦满足以上三个条件,那么就自动调用bgsave命令,同时更新lastsave属性和dirty属性归零。
至于检查保存条件是否满足这个工作,是由Redis服务器周期性操作函数serverCron默认间隔100毫秒执行一次检查,这个函数有很多地方用到,注意一下,这个函数是对正在运行的服务器进行维护的函数,在Redis事件中会有提到(Redis服务器是一个事件驱动程序,什么是事件驱动呢?就是发生事件的时候才会动一下,不然就跟死了一样,事件驱动又分为文件事件和时间事件,ServerCron就是一种时间驱动,至于文件驱动,其实就是客户端发过来一个命令,服务器才会去执行,然后给客户端返回结果)
最后说一点,Redis本身自带了一个RDB文件检查工具redis-check-dump,可以使用这个工具对rdb文件是否完整进行检查。
AOF方式
redis默认不开启AOF(append only file)持久化,可以通过以下参数开启:
appendonly yes
开启AOF之后,每执行一条会更改redis中数据的命令,都会将该命令写入硬盘中的AOF文件中,保存位置与RDB相同,文件名默认为appendonly.aof。
AOF文件是纯文本文件,其内容为redis客户端向redis发送的原始通信协议的内容。
虽然每次的修改都会记录到AOF文件中,但是由于操作系统的缓存机制,数据并没有真正写入硬盘,而是进入系统的硬盘缓存,默认情况每30秒才执行一次同步操作,因此系统异常退出时会丢失未同步的数据。为解决这个问题,redis需要在写入AOF文件后主动要求系统进行同步:
#appendfsync always
appendfsync everysec
#appendfsync no
复制
为避免单点故障,最好将数据库复制多个副本以部署在不同的服务器上,redis提供了复制(replication)功能可以自动实现同步的过程。
配置
主数据库(master)可以进行读写操作,当发生写操作时自动将数据同步给从数据库(slave)。
从数据库一般是只读的,它只能有一个主数据库。
配置非常简单:
主数据库无需任何配置,从数据库只需在配置文件中加入“slaveof 主数据库IP 主数据库端口”即可。
原理
从数据库启动后,先向主数据库发送SYNC命令;主数据库接到SYNC命令后就开始保存快照(即使关闭了RDB持久化方式),在此期间,所有发给主数据库的命令都被缓存起来;快照保存完成后,主数据库把快照和缓存的命令一起发给从数据库;从数据库保存主数据库发来的快照文件,并依次执行主数据库发来的缓存命令。在同步过程中,从数据库不会阻塞,它默认使用同步之前的数据继续响应客户端发来的命令。
之后,主数据的任何数据变化都是通过TCP同步给从数据库。
读写分离
可以通过复制功能建立多个从数据库,主数据库只进行写操作,从数据库负责读操作,以提高服务器的负载能力。
从数据库持久化
为提高性能,可通过复制功能建立一个从数据库,并在从数据库中启用持久化,而主数据库禁用持久化。
当从数据库崩溃时,重启后主数据库会将数据同步过来,无需担心数据丢失;
当主数据库崩溃时,在从数据库中使用SLAVEOF NO ONE命令将从数据库提升为主数据库继续服务,并在原来的主数据库重启后使用SLAVEOF命令将其设置为新的主数据库的从数据库,即可把数据同步回来。
安全
Redis数据库可以设置密码,相关的任何客户端都需要在执行命令之前进行身份验证。
密码可以通过配置文件的requirepass来设置。
例子
127.0.0.1:6379> CONFIG get requirepass
1) "requirepass"
2) ""
默认情况下,此属性为空,表示没有设置密码。可以通过执行以下命令来更改这个属性
127.0.0.1:6379> CONFIG set requirepass "jiange"
OK
127.0.0.1:6379> CONFIG get requirepass
1) "requirepass"
2) "jiange"
客户端需要使用AUTH命令进行认证。
127.0.0.1:6379> AUTH password
另外,redis可以修改bind参数来限制访问的地址。
结语
好啦,关于redis快速入门的内容就到这里了,接下来需要做的就是在实践中熟练地使用。
后期如果有需要,有时间,我将学习一下redis的源码实现( ̄▽ ̄)”。