18、聊聊redis(一)

Redis 内置了复制(Replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(Transactions) 和不同级别的磁盘持久化(Persistence),并通过 Redis哨兵(Sentinel)和自动分区(Cluster)提供高可用性(High Availability)。

redis的内存模型

与Memcached仅支持简单的key-value结构的数据记录不同,Redis支持的数据类型要丰富得多,常用的数据类型主要有五种:String、List、Hash(map)、Set和Sorted Set。

Redis内部使用一个redisObject对象来表示所有的key和value。redisObject主要的信息包括数据类型(type)、编码方式(encoding)、数据指针(ptr)、虚拟内存(vm)等。type代表一个value对象具体是何种数据类型,encoding是不同数据类型在redis内部式。

String类型

字符串是Redis值的最基础的类型。Redis中使用的字符串是通过包装的,基于c语言字符数组实现的简单动态字符串(simple dynamic string, SDS)一个抽象数据结构。其源码定义如下:

struct sdshdr {
    int len; //len表示buf中存储的字符串的长度。
    int free; //free表示buf中空闲空间的长度。
    char buf[]; //buf用于存储字符串内容。
};


假设上图是”hello”字符串的内存结构,这个时候len=5,free=2那么redis包装后(sds)其长度为:

sizeof(struct sdshdr) + len + free + 1

其中buf的大小为:

len + free + 1

1个字节是用来存储结束符’\0’的。Redis字符串是二进制安全的,因为二进制数据通常会有中间某个字节存储’\0’的这种情况,这意味着一个Redis字符串可以包含任何种类的数据,例如一个JPEG图像或者一个序列化的Ruby对象。二进制是否安全,简单的理解就是能不能在字符串中间有‘\0’,如下图:

对于上图,sds认为这个字符串是“hello world”,而C语言的字符处理函数认为这个字符串是“hello”。

String(字符串)

DECR //decr && decrby——decr对key对应的值进行减减操作,并返回新的值;decrby减指定值
DECRBY

INCR //incr && incrby——incr对key对应的值进行加加操作,并返回新的值;incrby加指定值
INCRBY  //incr age1    incrby age 3

SETEX  //设置key对应的值为String类型的value,并设定有效期,如果 key 已经存在, SETEX 命令将覆写旧值。
SETNX //设置key对应的值为String类型的value,如果key已经存在则返回0

测试:

127.0.0.1:6379> set age 20
OK
127.0.0.1:6379> set age1 "20"
OK
127.0.0.1:6379> incr age
(integer) 21
127.0.0.1:6379> incr age1
(integer) 21

我们对int型的age和string型的age1都能进行incr操作时,
实际上type=string代表value存储的是一个普通字符串,那么对应的encoding可以是raw或者是int,如果是int则代表实际redis内部是按数值型类存储和表示这个字符串的,当然前提是这个字符串本身可以用数值表示,比如”20”这样的字符串,当遇到incr、decr等操作时会转成数值型进行计算,此时redisObject的encoding字段为int。如果你试图对name进行incr操作则报错。

127.0.0.1:6379> set name "nick"
OK
127.0.0.1:6379> incr name
(error) ERR value is not an integer or out of range

Hash类型

Hash是一个String类型的field和value之间的映射表,即redis的Hash数据类型的key(hash表名称)对应的value实际的内部存储结构为一个HashMap,因此Hash特别适合存储对象。相对于把一个对象的每个属性存储为String类型,将整个对象存储在Hash类型中会占用更少内存。


当前HashMap的实现有两种方式:当HashMap的成员比较少时Redis为了节省内存会采用类似一维数组的方式来紧凑存储,而不会采用真正的HashMap结构,这时对应的value的redisObject的encoding为zipmap,当成员数量增大时会自动转成真正的HashMap,此时encoding为ht。

Hash(哈希表)
HDEL 删除key对应的HashMap中的field
HEXISTS
HGET
HGETALL 获取key对应的HashMap中的所有field的value
HINCRBY 给key对应的HashMap中的field的value加指定的值
HINCRBYFLOAT
HKEYS 返回key对应的HashMap中所有的field
HLEN 返回key对应的HashMap中的field的数量
HMGET  批量获取key对应的HashMap中的field的value
HMSET 批量设置key对应的HashMap中的field的value
HSET 
HSETNX 设置key对应的HashMap中的field的value,如果不存在则先创建
HVALS 返回key对应的HashMap中所有的value
HSCAN

测试:

127.0.0.1:6379> hset nick age 23
(integer) 1
127.0.0.1:6379> hset nick name "nick"
(integer) 1
127.0.0.1:6379> hget nick age
"23"
127.0.0.1:6379> hget nick name
"nick"
127.0.0.1:6379> hgetall nick
1) "age"
2) "23"
3) "name"
4) "nick"
127.0.0.1:6379> hincrby nick age 1
(integer) 24

List类型

Redis的List类型其实就是每一个元素都是String类型的双向链表。我们可以从链表的头部和尾部添加或者删除元素。这样的List既可以作为栈,也可以作为队列使用。

List(列表)
BLPOP
BRPOP
BRPOPLPUSH
LINDEX 返回key对应的list中index的元素
LINSERT 将值 value 插入到列表 key 当中,位于值 pivot 之前或之后。
LLEN 返回key对应的list的长度
LPOP 从key对应的list的头部删除一个元素,并返回该元素。
LPUSH 在key对应的list的头部添加一个元素
LPUSHX
LRANGE 获取key对应的list的指定下标范围的元素,-1表示获取所有元素
LREM
LSET 将列表 key 下标为 index 的元素的值设置为 value 。
LTRIM
RPOP
RPOPLPUSH
RPUSH
RPUSHX

测试:

127.0.0.1:6379> lpush students  zhangsan lisi xiaoming xiaohong
(integer) 4
127.0.0.1:6379> lrange students 0 -1
1) "xiaohong"
2) "xiaoming"
3) "lisi"
4) "zhangsan"
127.0.0.1:6379> lpop students
"xiaohong"
127.0.0.1:6379> rpop students
"zhangsan"
127.0.0.1:6379> lrange students 0 -1
1) "xiaoming"
2) "lisi"
127.0.0.1:6379> linsert students BEFORE "lisi" "nick"
(integer) 3
127.0.0.1:6379> lrange students 0 -1
1) "xiaoming"
2) "nick"
3) "lisi"

Set类型

Redis 集合(Set类型)是一个无序的String类型数据的集合,类似List的一个列表,与List不同的是Set不能有重复的数据。实际上,Set的内部是用HashMap实现的,Set只用了HashMap的key列来存储对象。java中HashSet创建一个HashSet的时候实际上创建了一个HashMap;Set中的元素,只是存放在了底层HashMap的key上,底层HashMap的value列为空,遍历HashSet的时候从HashMap中取出keySet来遍历。

    public HashSet(Collection<? extends E> c) {
        map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
        addAll(c);
    }

Set(集合)
SADD 在key对应的set中添加一个元素。 
SCARD 返回key对应的set的元素个数
SDIFF myset-myset2 的差集
SDIFFSTORE
SINTER myset && myset2 的交集
SINTERSTORE 
SISMEMBER 
SMEMBERS 获取key对应的set的所有元素。 
SMOVE 将 member 元素从 c1 集合移动到 c2 集合
SPOP 随机返回并删除key对应的set中的一个元素。
SRANDMEMBER
SREM 删除key对应的set中的一个元素
SUNION myset||myset2 的并集
SUNIONSTORE
SSCAN

测试:

127.0.0.1:6379> sadd names "nick" "xiaohong" "xiaoli"
(integer) 3
127.0.0.1:6379> smembers names
1) "xiaoli"
2) "xiaohong"
3) "nick"

SortSet

SortSet顾名思义,是一个排好序的Set,它在Set的基础上增加了一个顺序属性score,这个属性在添加修改元素时可以指定,每次指定后,SortSet会自动重新按新的值排序。
sorted set的内部使用HashMap和跳跃表(SkipList)来保证数据的存储和有序,HashMap里放的是成员到score的映射,而跳跃表里存放的是所有的成员,排序依据是HashMap里存的score。
(1)zadd ——在key对应的zset中添加一个元素
(2)zrange——获取key对应的zset中指定范围的元素,-1表示获取所有元素
(3)zrem——删除key对应的zset中的一个元素
测试:

127.0.0.1:6379> zadd names1  1 "nick" 2 "xiaohong" 3 "xiaoli"
(integer) 3
127.0.0.1:6379> zrange names1 0 -1
1) "nick"
2) "xiaohong"
3) "xiaoli"

应用场景 - 如按时间排序的时间轴

参考:http://doc.redisfans.com/

Redis为什么快?

Redis为什么这么快
1、完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1);

2、数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的;

3、采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;

4、使用多路I/O复用模型,非阻塞IO;

5、使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;

(1)多路 I/O 复用模型

多路I/O复用模型是利用 select、poll、epoll 可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有 I/O 事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll 是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。

这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络 IO 的时间消耗),且 Redis 在内存中操作数据的速度非常快,也就是说内存内的操作不会成为影响Redis性能的瓶颈,主要由以上几点造就了 Redis 具有很高的吞吐量。

redis 主从、事务与监控、持久化、发布以及订阅消息

安全性设置

设置客户端操作秘密
redis安装好后,默认情况下登陆客户端和使用命令操作时不需要密码的。某些情况下,为了安全起见,我们可以设置在客户端连接后进行任何操作之前都要进行密码验证。修改redis.conf进行配置。

# requirepass foobared
requirepass redis129

客户端授权方式

(1)登录时使用-a参数指定客户端密码,如下

[root@localhost ~]# /usr/local/redis/bin/redis-cli -h 192.168.2.129 -p 6379 -a redis129

(2)登录客户端后使用auth命令进行授权,如下

[root@localhost ~]# /usr/local/redis/bin/redis-cli -h 192.168.2.129 -p 6379
192.168.2.129:6379> auth redis129
OK

主从复制(读写分离)

主从复制,即主服务器与从服务器之间数据备份的问题。Redis 支持简单且易用的主从复制(master-slave replication)功能, 该功能可以让从服务器(slave server)成为主服务器(master server)的精确复制品。
主从复制的特点
(1)一个主服务器可以有多个从服务器。
(2)不仅主服务器可以有从服务器, 从服务器也可以有自己的从服务器。
(3)Redis 支持异步复制和部分复制(这两个特性从Redis 2.8开始),主从复制过程不会阻塞主服务器和从服务器。
(4)主从复制功能可以提升系统的伸缩性和功能,如让多个从服务器处理只读命令,使用复制功能来让主服务器免于频繁的执行持久化操作。

从上面的示意图可以看出,主服务器与从服务器建立连接之后,Redis主从复制过程主要有下面几步:
(1)从服务器都将向主服务器发送一个 SYNC 命令。
(2)主服务器接到 SYNC 命令后开启一个后台子进程并开始执行 BGSAVE,并在保存操作执行期间, 将所有新执行的写入命令都保存到一个缓冲区里面。
(3)当 BGSAVE 执行完毕后, 主服务器将执行保存操作所得的 .rdb 文件发送给从服务器, 从服务器接收这个 .rdb 文件, 并将文件中的数据载入到内存中。
(4)主服务器会以 Redis 命令协议的格式, 将写命令缓冲区中积累的所有内容都发送给从服务器。

BGSAVE
在后台异步(Asynchronously)保存当前数据库的数据到磁盘。

BGSAVE 命令执行之后立即返回 OK ,然后 Redis fork 出一个新子进程,原来的 Redis 进程(父进程)继续处理客户端请求,而子进程则负责将数据保存到磁盘,然后退出。

redis> BGSAVE
Background saving started

配置:

# slaveof <masterip> <masterport>
slaveof 192.168.2.129 6379

事务与监控

Redis 的事务支持相对简单,MULTI 、 EXEC 、 DISCARD 和 WATCH 这四个命令是 Redis 事务的基础。
事务开启与取消
l MULTI 开启一个事务。当客户端发出了MULTI 命令时,客户端和服务端的连接就进入了一个事务上下文的状态。MULTI 执行之后, 客户端可以继续向服务器发送任意多条命令, 这些命令不会立即被执行, 而是被放到一个队列中, 当 EXEC 命令被调用时, 所有队列中的命令才会被执行。

192.168.2.129:6379> multi
OK
192.168.2.129:6379> set name2 "lisi"
QUEUED

l EXEC 顺序执行事务队列中的命令。
l DISCARD 取消事务。当执行 DISCARD 命令时, 事务会被放弃, 事务队列会被清空, 并且客户端会从事务状态中退出。
l WATCH 对key值进行监控。 在 WATCH 执行之后, EXEC 执行之前, 有其他客户端修改了 key 的值, 那么当前客户端的事务就会失败。

程序需要做的, 就是不断重试这个操作, 直到没有发生碰撞(Crash)为止。

持久化机制

redis支持将内存中的数据周期性的写入磁盘或者把操作追加到记录文件中,这个过程称为redis的持久化。redis支持两种方式的持久化,一种是快照方式(snapshotting),也称RDB方式;两一种是追加文件方式(append-only file),也称AOF方式。RDB方式是redis默认的持久化方式。RDB与AOF的区别:RDB记录的是数据,数据恢复直接填充,AOF记录的是操作命令,数据恢复是把命令重新执行一遍。

RDB方式
RDB方式是将内存中的数据的快照以二进制的方式写入名字为 dump.rdb的文件中。我们对 Redis 进行设置, 让它根据设置周期性自动保存数据集。修改redis.conf文件,如下

#900秒内如果有超过1个key被修改则发起保存快照
save 900 1
#300秒内如果有超过10个key被修改则发起保存快照
save 300 10
#60秒内如果有超过1000个key被修改则发起保存快照
save 60 10000

dump.rdb文件默认生成在%REDIS_HOME%etc目录下(如/usr/local/redis/etc/),可以修改redis.conf文件中的dir指定dump.rdb的保存路径

# The filename where to dump the DB
dbfilename dump.rdb

AOF方式
RDB方式是周期性的持久化数据, 如果未到持久化时间点,Redis 因为某些原因而造成故障停机, 那么服务器将丢失最近写入、且仍未保存到快照中的那些数据。所以从redis 1.1开始引入了AOF方式,AOF 持久化记录服务器执行的所有写操作命令,并在服务器启动时,通过重新执行这些命令来还原数据集。 AOF 文件中的命令全部以 Redis 协议的格式来保存,新命令会被追加到文件的末尾。
AOF方式仍然有丢失数据的可能,因为收到写命令后可能并不会马上将写命令写入磁盘,因此我们可以修改redis.conf,配置redis调用write函数写入命令到文件中的时机。如下

#启用AOF方式
appendonly yes
#每次有新命令追加到 AOF 文件时就执行一次 fsync :非常慢,也非常安全
appendfsync always
#每秒 fsync 一次:足够快(和使用 RDB 持久化差不多),并且在故障时只会丢失 1 秒钟的数据
appendfsync everysec
#从不 fsync :将数据交给操作系统来处理。更快,也更不安全的选择
appendfsync no

持久化过程中,线程读写是不会阻塞的。

发布以及订阅消息

发送者(发送信息的客户端)不是将信息直接发送给特定的接收者(接收信息的客户端), 而是将信息发送给频道(channel), 然后由频道将信息转发给所有对这个频道感兴趣的订阅者。SUBSCRIBE 、 UNSUBSCRIBE 和 PUBLISH 三个命令实现了消息的发布与订阅。
Client1 订阅频道mychannel

192.168.2.129:6379> subscribe mychannel
Reading messages… (press Ctrl-C to quit)
1) “subscribe”
2) “mychannel”
3) (integer) 1

Client2发布频道mychannel与消息

192.168.2.129:6379> publish mychannel “message from channel1”
(integer) 1
192.168.2.129:6379>

Client1 订阅频道接收到的信息

1) “message”
2) “mychannel”
3) “message from channel1”

虚拟内存

首先说明下redis的虚拟内存与os的虚拟内存不是一码事,但是思路和目的都是相同的。就是暂时把不经常访问的数据从内存交换到磁盘中,从而腾出宝贵的 内存空间用于其他需要访问的数据。尤其是对于redis这样的内存数据库,内存总是不够用的。除了可以将数据分割到多个redis server外。另外的能够提高数据库容量的办法就是使用vm把那些不经常访问的数据交换的磁盘上。如果我们的存储的数据总是有少部分数据被经常访问,大 部分数据很少被访问,对于网站来说确实总是只有少量用户经常活跃。当少量数据被经常访问时,使用vm不但能提高单台redis server数据库的容量,而且也不会对性能造成太多影响。
但是Redis没有使用Linux提供的虚拟内存机制,它是实现了自己的虚拟内存机制,主要原因有两点:
(1)Linux虚拟内存的粒度过大,在Linux中使用4KB的页面,这对于Redis来说太大了,二Redis中的绝大多数对象都远远小于这个数值。
(2)Redis可以在把数据交换到磁盘上的时候进行适当的操作,比如压缩,通常保存到磁盘上的对象可以去除指针和对象元数据信息,一般压缩后的对象可以比内存中的对象小10倍。这样可以节省很多IO操作。

当然,并不是所有场景都适合虚拟内存。这里需要注意的就是Redis中的Key是不会被交换的,如果每个key所关联的value都很小,那么这种场景就不太适合于使用虚拟内存了。如果key比较小,但是对应的value比较大,那么这种场景是最适合使用虚拟内存的场景。
下面是vm相关配置

(1)vm-enabled yes 表示在服务器启动时开启虚拟内存的功能。
(2)vm-max-memory 它是Redis使用的最大内存上限,它以字节为单位
(3)vm-pages 表示最多使用多少个页面
(4)vm-page-size 表示每个页面的大小,以字节为单位
(5)vm-max-threads 它是用于执行value对象换入换出的工作线程数量 //线程式虚拟内存 vs 阻塞式虚拟内存

接下来就是Redis的几个规定:
(1)Redis为了保证key的查找速度,只会将value交换到swap文件中。
(2)Redis在进行数据交换的时候,也是使用页面来交换的,而且Redis规定一个页面只能保存一个对象,但是一个对象可以保存到多个页面中。
(3)Redis使用的内存没有超过vm-max_memory之前是不会交换任何的value的,当超过最大内存的限制之后,Redis会选择过期的对象,如果两个对象过期时间一样,那么它会优先交换比较大的对象。
(4)如果vm-page-size设置的太小,会造成交换文件出现碎片,太大又会浪费空间。
(5)对于交换文件的每个页面,Redis都会在内存中对应一个bit值来记录页面的空闲状态。
(6)而vm-max-threads用来表示用作交换任务的线程数,如果大于0的话,则推荐为CPU的核的数量,如果是0则交换过程在主线程中执行
(7)对Redis而言,如果操作交换文件是以同步的方式进行的,那么当某一客户端正在访问交换文件的数据时,如果其他客户端也试图去访问交换文件中的数据,那么后面的这个客户端的请求会被挂起,直到之前的操作结束为止。如果在比较忙的时候读取较大的时,这种阻塞带来的后果会更严重。

开启虚拟内存的Redis :.rdb文件还是 AOF(Append Only File)文件更合适 ?
当虚拟内存开启时,保存和读取数据库操作都将变慢。当服务器被配置为用最少的内存时(即vm-max-memory 被设置为0),一个通常2s载入一次的DB操作,在开启虚拟内存时耗时将长达13s,所以你可能希望切换使用AOF的配置来实现持久化,以便可以进行BGREWRITEAOF操作。

请注意 当进程进行 BGSAVE 或者 BGREWRITEAOF 操作时,Redis不会在磁盘上交换新的value.
当有子进程访问虚拟内存时,虚拟内存将是只读的。所以,当有一个子进程有大量的写操作时,内存使用将增加。

虚拟内存的监控
一旦使用了开启虚拟内存的Redis,你可能很感兴趣它是怎样工作的:总共多少个对象被交换,每秒交换与载入的对象量等等。
下面是一个用于检查VM是如何工作的工具(见此处)。作为Redis 工具的一部分,redis-stat简单易用:
$ ./redis-stat vmstat

##redis数据分区
分区是分割数据到多个Redis实例的处理过程,因此每个实例只保存key的一个子集。
redis集群的一种应用。
Redis 集群没有并使用传统的一致性哈希来分配数据,而是采用另外一种叫做哈希槽 (hash slot)的方式来分配的。redis cluster 默认分配了 16384 个slot,当我们set一个key 时,会用CRC16算法来取模得到所属的slot,然后将这个key 分到哈希槽区间的节点上,具体算法就是:CRC16(key) % 16384。所以我们在测试的时候看到set 和 get 的时候,直接跳转到了7000端口的节点。

Redis 集群会把数据存在一个 master 节点,然后在这个 master 和其对应的salve 之间进行数据同步。当读取数据时,也根据一致性哈希算法到对应的 master 节点获取数据。只有当一个master 挂掉之后,才会启动一个对应的 salve 节点,充当 master 。

需要注意的是:必须要3个或以上的主节点,否则在创建集群时会失败,并且当存活的主节点数小于总节点数的一半时,整个集群就无法提供服务了。

参考:http://www.redis.net.cn/tutorial/3524.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值