redis入门笔记

redis入门笔记

参考redis实战手册

 

1. Redis在windows下安装

下载地址:https://github.com/MSOpenTech/redis/tags

安装Redis

1.1.这里下载的是Redis-x64-3.2.100版本,我的电脑是win7 64位,所以下载64位版本的,在运行中输入cmd,然后把目录指向解压的Redis目录。

1.2、启动命令

redis-server redis.windows.conf,出现下图显示表示启动成功了。此cmd窗口用来开启redis服务,不要关闭

 

 

1.3.连接redis命令

另外开启一个cmd窗口,输入redis-cli -h localhost -p 6379 

 

2.redis简介

Redis是一个Key-Value存储系统。和Memcached类似,它支持存储的value类型相对更多, 包括string(字符串)、hash、list(链表)、set(集合)和zset(有序集合)。这些数据类型都支持push/pop、 add/remove及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的。在此基础 上,Redis支持各种不同方式的排序。与memcached —样,为了保证效率,数据都是缓存在内存中。区别的是Redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录 文件,并且在此基础上实现了master-slave(主从)同步。

2.1 特点

下面简单的罗列了一 些特点:

Key-value store: 一个key-value数据存储系统,只支持一些基本操作,如:SET(key, value)和 GET(key)等;

分布式:多台机器(nodes)同时存储数据和状态,彼此交换消息来保持数据一致,可视为一个完整的存储系统。

数据一致:所有机器上的数据都是同步更新的、不用担心得到不一致的结果;

冗余:所有机器(nodes)保存相同的数据,整个系统的存储能力取决于单台机器(node) 的能力;

容错:如果有少数nodes出错,比如重启、当机、断网、网络丢包等各种fault/fail都不影响整个系统的运行;

高可靠性:容错、冗余等保证了数据库系统的可靠性。

2.2 适用场合

下面是Redis适用的一些场景:

1、取最新N个数据的操作

比如典型的取你网站的最新文章,通过下面方式,我们可以将最新的5000条评论的ID放在 Redis的List集合中,并将超出集合部分从数据库获取。

使用LPUSH latest.comments<ID>命令,向list集合中插入数据 插入完成后再用LTRIM latest.comments 0 5000命令使其永远只保存最近5000个ID 

如果你还有不同的筛选维度,比如某个分类的最新N条,那么你可以再建一个按此分类的 List,只存ID的话,Redis是非常高效的。

2、排行榜应用,取TOP N操作

这个需求与上面需求的不同之处在于,前面操作以时间为权重,这个是以某个条件为权重, 比如按顶的次数排序,这时候就需要我们的sorted set出马了,将你要排序的值设置成sorted set的score,将具体的数据设置成相应的value,每次只需要执行一条ZADD命令即可。

3、需要精准设定过期时间的应用

比如你可以把上面说到的sorted set的score值设置成过期时间的时间戳,那么就可以简单地通过过期时间排序,定时清除过期数据了,不仅是清除Redis中的过期数据,你完全可以把Redis里这个过期时间当成是对数据库中数据的索引,用Redis来找出哪些数据需要过期删除,然后再精准地从数据库中删除相应的记录。

4、计数器应用

Redis的命令都是原子性的,你可以轻松地利用INCR,DECR命令来构建计数器系统。

5、Uniq操作,获取某段时间所有数据排重值

这个使用Redis的set数据结构最合适了,只需要不断地将数据往set中扔就行了,set意为集合,所以会自动排重。

6、实时系统,反垃圾系统

通过上面说到的set功能,你可以知道一个终端用户是否进行了某个操作,可以找到其操作的集合并进行分析统计对比等。没有做不到,只有想不到。

7、Pub/Sub构建实时消息系统

Redis的Pub/Sub系统可以构建实时的消息系统,比如很多用Pub/Sub构建的实时聊天系统 的例子。

8、构建队列系统

使用list可以构建队列系统,使用sorted set甚至可以构建有优先级的队列系统。

9、缓存

这个不必说了,性能优于Memcached,数据结构更多样化。

2.3 发布及订阅消息

发布订阅(pub/sub)是一种消息通信模式,主要的目的是解耦消息发布者和消息订阅者之间的耦合,这点和设计模式中的观察者模式比较相似。pub/sub不仅仅解决发布者和订阅者直接代码级别耦合也解决两者在物理部署上的耦合。redis作为一个pub/sub的server,在订阅者和发布者之间起到了消息路由的功能。订阅者可以通过subscribe和psubscribe命令向redis server订阅自己感兴趣的消息类型,redis将消息类型称为通道(channel)。当发布者通过publish命令向redis server发送特定类型的消息时。订阅该消息类型的全部client都会收到此消息。这里消息的传递是多对多的。一个client可以订阅多个channel,也可以向多个channel发送消息。

 

下面做个实验。这里使用3不同的client, client1用于订阅tv1这个channel的消息,client2 用于订阅tv1和tv2这2个chanel的消息,client3用于发布tv1和tv2的消息。

        

 

 

下面将详细的解释一下上面的例子

1、client1订阅了tv1这个channel这个频道的消息,client2订阅了tv1和tv2这2个频道的消息

2、client3是用于发布tv1和tv2这2个频道的消息发布者

3、接下来我们在client3发布了一条消息”publish tv1 program1”,大家可以看到这条消息是发往 tv1 这个频道的

4、理所当然的client1和client2都接收到了这个频道的消息

5、然后client3又发布了一条消息”publish tv2 program2”,这条消息是发往tv2的,由于client1并没有订阅tv1,所以client1的结果中并没有显示出任何结果,但client2订阅了这个频道,所以client2是会有返回结果的。 我们也可以用psubscribe tv*的方式批量订阅以tv开头的频道的内容。

看完这个小例子后应该对pub/sub功能有了一个感性的认识。需要注意的是当一个连接通过subscribe或者psubscribe订阅通道后就进入订阅模式。在这种模式除了再订阅额外的通道或者用unsubscribe或者punsubscribe命令退出订阅模式,就不能再发送其他命令。另外使用 psubscribe命令订阅多个通配符通道,如果一个消息匹配上了多个通道模式的话,会多次收到同一个消息。

 

2.4 主从复制配置及原理

2.4.1 配置过程

下载windows环境redis,解压,拷贝一主(master)两从(slaveof)共三个redis安装目录。主机端口使用6379,两从的端口分别为6378和6377, 修改redis.windows.conf的port端口配置。

在两个从redis的redis.windows.conf中增加slaveof 127.0.0.1 6379和 masterauth beijing(主redis的密码是beijing)。

启动主从redis服务  开启三个cmd窗口不关闭,保持redis服务端在运行

主redis服务 端口 6379

 从redis服务 端口6377

 从redis服务  端口6378

  

 

启动主从redis的三个客户端,启动三个cmd窗口。在主redis设置参数,看是否在从redis取到值

连接主redis客户端并设置参数

连接从redis客户端并尝试取值

 

说明主从复制已经起到作用。

2.4.2 主从复制原理

redis主从复制配置和使用都非常简单。通过主从复制可以允许多个slave server拥有和master server相同的数据库副本。

redis主从复制特点:

(1)、master可以拥有多个slave

(2)、多个slave可以连接同一个master外,还可以连接到其他slave

(3)、主从复制不会阻塞master,在同步数据时,master可以继续处理client请求

(4)、提高系统的伸缩性

redis主从复制过程:

当配置好slave后,slave与master建立连接,然后发送sync命令。无论是第一次连接还是重新连接,master都会启动一个后台进程,将数据库快照保存到文件中,同时master主进程会开始收集新的写命令并缓存。后台进程完成写文件后,master就发送文件给slave,slave 将文件保存到硬盘上,再加载到内存中,接着master就会把缓存的命令转发给slave,后续master将收到的写命令发送给slave。如果master同时收到多个slave发来的同步连接命令,master只会启动一个进程来写数据库镜像,然后发送给所有的slave。

2.5 事务控制

redis对事务的支持目前还比较简单。redis只能保证一个client发起的事务中的命令可以连续的执行,而中间不会插入其他client的命令。由于redis是单线程来处理所有client的请求的所以做到这点是很容易的。一般情况下redis在接受到一个client发来的命令后会立即处理并返回处理结果,但是当一个client在一个连接中发出multi命令后,这个连接会进入一个事务上下文,该连接后续的命令并不是立即执行,而是先放到一个队列中。当从此连接受到exec命令后,redis会顺序的执行队列中的所有命令。并将所有命令的运行结果打包到一起返回给client.然后此连接就结束事务上下文。

2.5.1 简单事务控制

 

 

从这个例子我们可以看到2个set age命令发出后并没执行而是被放到了队列中。调用exec后2个命令才被连续的执行,最后返回的是两条命令执行后的结果。

2.5.2 如何取消事务

我们可以调用discard命令来取消一个事务,让事务回滚。接着上面例子

redis 127.0.0.1:6379> get age
"20"
redis 127.0.0.1:6379> multi
OK
redis 127.0.0.1:6379> set age 30
QUEUED
redis 127.0.0.1:6379> set age 40
QUEUED
redis 127.0.0.1:6379> discard
OK
redis 127.0.0.1:6379> get age
"20"
redis 127.0.0.1:6379>

可以发现这次2个set age命令都没被执行。discard命令其实就是清空事务的命令队列并退出事务上下文,也就是我们常说的事务回滚。

2.5.3 乐观锁复杂事务控制

在本小节开始前,我们有必要向读者朋友简单介绍一下乐观锁的概念,并举例说明乐观锁是怎么工作的。

乐观锁:大多数是基于数据版本(version)的记录机制实现的。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表添加一个“version”字段来实现读取出数据时,将此版本号一同读出,之后更新时,对此版本号加1。此时,将提交数据的版本号与数据库表对应记录的当前版本号进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。

乐观锁实例:假设数据库中帐户信息表中有一个version字段,当前值为1;而当前帐户余额字段(balance)为$100。下面我们将用时序表的方式来为大家演示乐观锁的实现原理:

 

操作员A

 

 

操作员B

 

 

(1)、操作员A此时将用户信息读出(此时version=1),并准备从其帐户余额中扣除$50 ($100-$50)

 

 

(2)、在操作员A操作的过程中,操作员B也读入此用户信息(此时version=1),并准备从其帐户余额中扣除$20 ($100-$20)

 

 

(3)、操作员A完成了修改工作,将数据版本号加1 (此时version=2),连同帐户扣除后余额(balance=$50),提交至数据库更新,此时由于提交数据版本大于数据库记录当前版本,数据被更新,数据库记录version 更新为2

 

 

 

 

(4)、操作员B完成了操作,也将版本号加1 (version=2 )并试图向数据库提交数据(balance=$80),但此时比对数据库记录版本时发现,操作员B提交的数据版本号为2,数据库记录当前版本也为2,不满足“提交版本必须大于记录当前版本才能执行更新”的乐观锁策略,因此,操作员B的提交被驳回

 

 

这样,就避免了操作员B用基于version=1的旧数据修改的结果来覆盖操作员A的操作结果的可能。

即然乐观锁比悲观锁要好很多,redis是否也支持呢?答案是支持,redis从2.1.0开始就支持乐观锁了,可以显式的使用watch对某个key进行加锁,避免悲观锁带来的一系列问题。Redis乐观锁实例:假设有一个age的key,我们开2个session来对age进行赋值操作,我们来看一下结果如何。

 

Session 1

 

Session 2

 

(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

redis 127.0.0.1:6379>

 

 

 

(2)第2步

redis 127.0.0.1:6379> set age 30

OK

redis 127.0.0.1:6379> get age

"30"

redis 127.0.0.1:6379>

 

(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"

redis 127.0.0.1:6379>

 

 

 

从以上实例可以看到在

第一步,Session 1还没有来得及对age的值进行修改

第二步,Session 2已经将age的值设为30

第三步,Session 1希望将age的值设为20,但结果一执行返回是nil,说明执行失败,之后我们再取一下age的值是30,这是由于Session 1中对age加了乐观锁导致的。

watch命令会监视给定的key,当exec时候如果监视的key从调用watch后发生过变化,则整个事务会失败。也可以调用watch多次监视多个key.这样就可以对指定的key加乐观锁了。注意watch的key是对整个连接有效的,事务也一样。如果连接断开,监视和事务都会被自动清除。当然了exec,discard,unwatch命令都会清除连接中的所有监视。

redis的事务实现是如此简单,当然会存在一些问题。第一个问题是redis只能保证事务的每个命令连续执行,但是如果事务中的一个命令失败了,并不回滚其他命令,比如使用的命令类型不匹配。下面将以一个实例的例子来说明这个问题:

redis 127.0.0.1:6379>get age

"30"

redis 127.0.0.1:6379>get name

"HongWan"

redis 127.0.0.1:6379>multi

OK

redis 127.0.0.1:6379> incr age

QUEUED

redis 127.0.0.1:6379> incr name

QUEUED

redis 127.0.0.1:6379> exec

1) (integer) 31

2) (error) ERR value is not an integer or out of range

redis 127.0.0.1:6379> get age

"31"

redis 127.0.0.1:6379> get name

"HongWan"

redis 127.0.0.1:6379>

从这个例子中可以看到,age由于是个数字,那么它可以有自增运算,但是name是个字符串,无法对其进行自增运算,所以会报错,如果按传统关系型数据库的思路来讲,整个事务都会回滚,但是我们看到redis却是将可以执行的命令提交了,所以这个现象对于习惯于关系型数据库操作的朋友来说是很别扭的,这一点也是redis今天需要改进的地方。

2.6 持久化机制

redis是一个支持持久化的内存数据库,也就是说redis需要经常将内存中的数据同步到磁盘来保证持久化。redis支持两种持久化方式,一种是Snapshotting (快照)也是默认方式,另一种是Append-only file (缩写aof)的方式。下面分别介绍:

snapshotting 方式

快照是默认的持久化方式。这种方式是就是将内存中数据以快照的方式写入到二进制文件中, 默认的文件名为dump.rdb。可以通过配置设置自动做快照持久化的方式。我们可以配置redis 在n秒内如果超过m个key被修改就自动做快照,下面是默认的快照保存配置

save 900 1 #900秒内如果超过1个key被修改,则发起快照保存

save 300 10 #300秒内容如超过10个key被修改,则发起快照保存

下面介绍详细的快照保存过程:

1.redis调用fork,现在有了子进程和父进程。

2.父进程继续处理client请求,子进程负责将内存内容写入到临时文件。由于os的实时复制机制(copy on write)父子进程会共享相同的物理页面,当父进程处理写请求时os会为父进程要修改的页面创建副本,而不是写共享的页面。所以子进程地址空间内的数据是fork时刻整个数据库的一个快照。

3. 当子进程将快照写入临时文件完毕后,用临时文件替换原来的快照文件,然后子进程退出。client也可以使用save或者bgsave命令通知redis做一次快照持久化。save操作是在主线程中保存快照的,由于redis是用一个主线程来处理所有client的请求,这种方式会阻塞所有client请求。所以不推荐使用。另一点需要注意的是,每次快照持久化都是将内存数据完整写入到磁盘一次,并不是增量的只同步变更数据。如果数据量大的话,而且写操作比较多,必然会引起大量的磁盘io操作,可能会严重影响性能。

aof 方式

另外由于快照方式是在一定间隔时间做一次的,所以如果redis意外down掉的话,就会丢失最后一次快照后的所有修改。如果应用要求不能丢失任何修改的话,可以采用aof持久化方式。下面介绍Append-only file:

aof比快照方式有更好的持久化性,是由于在使用aof持久化方式时,redis会将每一个收到的写命令都通过write函数追加到文件中(默认是appendonly.aof)。当redis重启时会通过重新执行文件中保存的写命令来在内存中重建整个数据库的内容。当然由于os会在内核中缓存write做的修改,所以可能不是立即写到磁盘上。这样aof方式的持久化也还是有可能会丢失部分修改。不过我们可以通过配置文件告诉redis我们想要通过fsync函数强制os写入到磁盘的时机。有三种方式如下(默认是:每秒fsync —次)

appendonly yes     //启用aof持久化方式

#appendfsync always  //收到写命令就立即写入磁盘,最慢,但是保证完全的持久化

#appendfsync everysec //每秒钟写入磁盘一次,在性能和持久化方面做了很好的折中

#appendfsync no   //完全依赖os,性能最好,持久化没保证

aof的方式也同时带来了另一个问题。持久化文件会变的越来越大。例如我们调用incr test 命令100次,文件中必须保存全部的100条命令,其实有99条都是多余的。因为要恢复数据库的状态其实文件中保存一条set test 100就够了。为了压缩aof的持久化文件。redis提供了bgrewriteaof命令。收到此命令redis将使用与快照类似的方式将内存中的数据以命令的方式保存到临时文件中,最后替换原来的文件。具体过程如下

1、redis调用fork,现在有父子两个进程

2、子进程根据内存中的数据库快照,往临时文件中写入重建数据库状态的命令

3、父进程继续处理client请求,除了把写命令写入到原来的aof文件中。同时把收到的写命令缓存起来。这样就能保证如果子进程重写失败的话并不会出问题。

4、当子进程把快照内容写入已命令方式写到临时文件中后,子进程发信号通知父进程。然后父进程把缓存的写命令也写入到临时文件。

5、现在父进程可以使用临时文件替换老的aof文件,并重命名,后面收到的写命令也开始往新的aof文件中追加。需要注意到是重写aof文件的操作,并没有读取旧的aof文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的aof文件,这点和快照有点类似。

2.7 Pipeline  批量发送请求

redis是一个cs模式的tcp server,使用和http类似的请求响应协议。一个client可以通过一个socket连接发起多个请求命令。每个请求命令发出后client通常会阻塞并等待redis服务处理,redis处理完后请求命令后会将结果通过响应报文返回给client。基本的通信过程如下:

Client: INCR X

Server: 1

Client: INCR X

Server: 2

Client: INCR X

Server: 3

Client: INCR X

Server: 4

基本上四个命令需要8个tcp报文才能完成。由于通信会有网络延迟,假如从client和server 之间的包传输时间需要0.125秒。那么上面的四个命令8个报文至少会需要1秒才能完成。这样即使redis每秒能处理100个命令,而我们的client也只能一秒钟发出四个命令。这显示没有充分利用redis的处理能力,怎么样解决这个问题呢?我们可以利用pipeline的方式从client打包多条命令一起发出,不需要等待单条命令的响应返回,而redis服务端会处理完多条命令后会将多条命令的处理结果打包到一起返回给客户端。通信过程如下

Client:

INCR X

Client:

INCR X

Client:

INCR X

Client:

INCR X

Server: 1

Server:

2

Server:

3

Server:

4

 

假设不会因为tcp报文过长而被拆分。可能两个tcp报文就能完成四条命令,client可以将四个incr命令放到一个tcp报文一起发送,server则可以将四条命令的处理结果放到一个tcp 报文返回。通过pipeline方式当有大批量的操作时候,我们可以节省很多原来浪费在网络延

迟的时间,需要注意到是用pipeline方式打包命令发送,redis必须在处理完所有命令前先缓存起所有命令的处理结果。打包的命令越多,缓存消耗内存也越多。所以并不是打包的命令越多越好。具体多少合适需要根据具体情况测试。

用两种方式发送指令,耗时是不一样的,具体是否使用pipeline必须要基于大家手中的网络情况来决定,不能一切都按最新最好的技术来实施,因为它有可能不是最适合你的。

 

2.8 虚拟内存的使用

首先说明下redis的虚拟内存与操作系统的虚拟内存不是一码事,但是思路和目的都是相同的。就是暂时把不经常访问的数据从内存交换到磁盘中,从而腾出宝贵的内存空间用于其他需要访问的数据。尤其是对于redis这样的内存数据库,内存总是不够用的。除了可以将数据分割到多个redis server外。另外的能够提高数据库容量的办法就是使用虚拟内存把那些不经常访问的数据交换的磁盘上。如果我们的存储的数据总是有少部分数据被经常访问,大部分数据很少被访问,对于网站来说确实总是只有少量用户经常活跃。当少量数据被经常访问时,使用虚拟内存不但能提高单台redis server数据库的容量,而且也不会对性能造成太多影响。

redis没有使用操作系统提供的虚拟内存机制而是自己在实现了自己的虚拟内存机制,主要的理由有两点:

1 、操作系统的虚拟内存是已4k页面为最小单位进行交换的。而redis的大多数对象都远小于4k,所以一个操作系统页面上可能有多个redis对象。另外redis的集合对象类型如list,set 可能存在与多个操作系统页面上。最终可能造成只有10%key被经常访问,但是所有操作系统页面都会被操作系统认为是活跃的,这样只有内存真正耗尽时操作系统才会交换页面。

2 、相比于操作系统的交换方式,redis可以将被交换到磁盘的对象进行压缩,保存到磁盘的对象可以去除指针和对象元数据信息,一般压缩后的对象会比内存中的对象小10倍,这样redis 的虚拟内存会比操作系统虚拟内存能少做很多io操作。

下面是vm相关配置

vm-enabled yes #开启vm 功能
vm-swap-file /tmp/redis.swap #交换出来的 value 保存的文件路径
vm-max-memory 1000000 #redis 使用的最大内存上限
vm-page-size 32 #每个页面的大小 32 个字节
vm-pages 134217728 #最多使用多少页面
vm-max-threads 4 #用于执行 value 对象换入换出的工作线程数量

redis的虚拟内存在设计上为了保证key的查找速度,只会将value交换到swap文件中。所以如果是内存问题是由于太多value很小的key造成的,那么虚拟内存并不能解决,和操作系统一样redis也是按页面来交换对象的。redis规定同一个页面只能保存一个对象。但是一个对象可以保存在多个页面中。在redis使用的内存没超过vm-max-memory之前是不会交换任何value的。当超过最大内存限制后,redis会选择较过期的对象。如果两个对象一样过期会优先交换比较大的对象,精确的公式swappability = age*log(size_in_memory)。对于vm-page-size的设置应该根据自己的应用将页面的大小设置为可以容纳大多数对象的大小,太大了会浪费磁盘空间,太小了会造成交换文件出现碎片。对于交换文件中的每个页面,redis 会在内存中对应一个1bit值来记录页面的空闲状态。所以像上面配置中页面数量(vm-pages 134217728 )会占用16M内存用来记录页面空闲状态。vm-max-threads表示用做交换任务的线程数量。如果大于0推荐设为服务器的cpu内核的数量,如果是0则交换过程在主线程进行。

 

posted on 2018-03-06 15:35  秦羽的思考  阅读(338)  评论(0编辑  收藏  举报