Redis

 

Redis

Redis底层结构:

         Redis 是支持多key-value数据库(表)的,并用 RedisDb 来表示一个key-value数据库(表). redisServer 中有一个 redisDb *db,成员变量, RedisServer 在初始化时,会根据配置文件的 db 数量来创建一个redisDb 数组. 客户端在连接后,通过 SELECT 指令来选择一个 reidsDb,如果不指定,则缺省是redisDb数组的第1个(即下标是 0 ) redisDb. 一个客户端在选择 redisDb 后,其后续操作都是在此redisDb 上进行的.下面是redisDb 的内存结构图:

 

 

 

redisDb 中 ,dict 成员是与实际存储数据相关的,dict成员即字典,简单说就是存储key-value键值数据,当然value=NULL那么就是集合了。字典通俗来说就是C++ STL中的map,STL中的map是用red-black tree实现的,因为map不仅能够保证key不重复,而且key还是按照字典序存储的,而Redis中的字典并不要求有序,因此为了降低编码的难度使用哈希表作为字典的底层实现。

Redis的字典是使用一个桶bucket,通过对key进行hash得到的索引值index,然后将key-value的数据存在桶的index位置,Redis处理hash碰撞的方式是链表,两个不同的key hash得到相同的索引值,那么就使用链表解决冲突。使用链表自然当存储的数据巨大的时候,字典不免会退化成多个链表,效率大大降低,Redis采用rehash的方式对桶进行扩容来解决这种退化。字典结构图:

 

 

字典添加元素:

根据字典当前的状态,将一个key-value元素添加到字典中可能会引起一系列复制的操作:如果字典未初始化(即字典的0号哈希表ht[0]的table为空),那么需要调用dictExpand函数对它初始化;

如果插入的元素key已经存在,那么添加元素失败;

如果插入元素时,引起碰撞,需要使用链表来处理碰撞;

如果插入元素时,引起程序满足Rehash的条件时,先调用dictExpand函数扩展哈希表的size,然后准备渐进式Rehash操作。

Rehash的触发机制:当每次添加新元素时,都会对工作哈希表ht[0]进行检查,如果used(哈希表中元素的数目)与size(桶的大小)比率ratio满足以下任一条件,将激活字典的Rehash机制:ratio=used / size, ratio >= 1并且dict_can_resize 为真;ratio 大 于 变 量 dict_force_resize_ratio 。

Rehash执行过程:

创建一个比ht[0].used至少两倍的ht[1].table;将原ht[0].table中所有元素迁移到ht[1].table;清空原来ht[0],将ht[1]替换成ht[0]

渐进式Rehash主要由两个函数来进行:

_dictRehashStep:当对字典进行添加、查找、删除、随机获取元素都会执行一次,其每次在开始Rehash后,将ht[0].table的第一个不为空的索引上的所有节点全部迁移到ht[1].table;

dictRehashMilliseconds:由Redis服务器常规任务程序(serverCron)执行,以毫秒为单位,在一定时间内,以每次执行100步rehash操作。

Redis 五种对象类型

在redis中value可以存储5种对象类型

字符串类型(String):

   当value的encenconding为REDIS_ENCODING_INT即redis会尝试将一个字符串转化为Long,可以转换的话,即保存为REDIS_ENCODING_INT否则,Redis会将REDIS_STRING保存为字符串类型,即REDIS_ENCODING_RAW.

主要为了解决长度计算和追加效率的问题.

Hash表

REDIS_HASH可以有两种encoding方式:

 REDIS_ENCODING_ZIPMAP和 REDIS_ENCODING_HTREDIS_ENCODING_HT即前文提到的字典的实现

REDIS_ENCODING_ZIPMAP即ZIPMAP,是一种双端列表,且通过特殊的格式定义,压缩内存适用,以时间换空间。ZIPLIST中的哈希对象是按照key1,value1,key2,value2这样的顺序存放来存储的。ZIPLIST适合小数据量的读场景,不适合大数据量的多写/删除场景

Hash表默认的编码格式为REDIS_ENCODING_ZIPLIST,在收到来自用户的插入数据的命令时:

1,调用hashTypeTryConversion函数检查键/值的长度大于 配置的hash_max_ziplist_value(默认64)

2,调用hashTypeSet判断节点数量大于 配置的hash_max_ziplist_entries (默认512)

以上任意条件满足则将Hash表的数据结构从REDIS_ENCODING_ZIPMAP转为REDIS_ENCODING_HT列表

Lists(列表)

REDIS_SET有两种encoding方式,REDIS_ENCODING_ZIPLIST和REDIS_ENCODING_LINKEDLIST

REDIS_ENCODING_ZIPLIST同上

REDIS_ENCODING_LINKEDLIST是比较正统双端链接表的实现

列表的默认编码格式为REDIS_ENCODING_ZIPLIST,当满足以下条件时,编码格式转换为REDIS_ENCODING_LINKEDLIST

1,元素大小大于list-max-ziplist-value(默认64)

2,元素个数大于 配置的list-max-ziplist-entries(默认512)

Sets(集合)

REDIS_SET有两种encoding方式:REDIS_ENCODING_INTSET 和 REDIS_ENCODING_HT(同上)

intset 是用一个有序的整数数组来实现集合(set)数组是 int16_t 类型, int32_t 类型还是 int64_t 类型的数组. 至于怎么选择是那种类型的数组,是根据其保存的值的取值范围来决定的,初始化时是 int16_t, 根据 set 中的最大值在 [INT16_MIN, INT16_MAX] , [INT32_MIN, INT32_MAX], [INT64_MIN,INT64_MAX]的那个取值范围来动态确定整个数组的类型. 例如set一开始是 int16_t 类型,当一个取值范围在 [INT32_MIN, INT32_MAX]的值加入到 set 时,则将保存 set 的数组升级成int32_t 的数组

集合的元素类型和数量决定了encoding方式,默认采用REDIS_ENCODING_INTSET ,当满足以下条件时,转换为REDIS_ENCODING_HT:

1. 元素类型不是整数

2. 元素个数超过配置的“set-max-intset-entries”(默认512)

REDIS_ENCODING_INTSET是一个有序数组

 

Zset(有序集)

REDIS_ZSET有两种encoding方式:REDIS_ENCODING_ZIPLIST(同上)和 REDIS_ENCODING_SKIPLIST

REDIS_ENCODING_SKIPLIST由于有续集每一个元素包括:<member,score>两个属性,为了保证对member和score都有很好的查询性能,REDIS_ENCODING_SKIPLIST同时采用字典和有序集两种数据结构来保存数据元素。字典和有序集通过指针指向同一个数据节点来避免数据冗余。

字典中使用member作为key,score作为value,从而保证在O(1)时间对member的查找

跳跃表基于score做排序,从而保证在 O(logN) 时间内完成通过score对memer的查询

有续集默认也是采用REDIS_ENCODING_ZIPLIST的实现,当满足以下条件时,转换为REDIS_ENCODING_SKIPLIST

1. 数据元素个数超过配置的zset_max_ziplist_entries 的值(默认值为 128 )

2. 新添加元素的 member 的长度大于配置的zset_max_ziplist_value 的值(默认值为 64 )

 

Redis 持久化

RDB(快照) :

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

默认的快照保存:

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

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

save 60 10000 #60秒内容如果超过10000个key被修改,则发起快照保存。

快照保存过程:

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

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

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

另外由于快照方式是在一定间隔时间做一次的,所以如果redis意外down掉的话,就会丢失最后一次快照后的所有修改数据。如果应用要求不能丢失任何修改的话就不适合用快照做持久化。

 

AOF(Append-Only File):

      使用AOF持久化方式时,Redis会将每一个收到的写命令都通过Write函数追加到文件中。

当Redis重启是会通过重新执行文件中保存的写命令来在内存中重建整个数据库的内容。

   默认配置: ppendonly yes #启用aof持久化方式appendfsync always #每次收到写命令就立即强制写入磁盘,最慢的,但是保证完全的持久化。

appendfsync everysec #每秒钟强制写入磁盘一次。持久化过程:

AOF 后台执行的方式和 RDB 有类似的地方,fork一个子进程,主进程仍进行服务,子进程执行 AOF 持久化,数据被 dump 到磁盘上。与 RDB 不同的是,后台子进程持久化过程中,主进程会记录期间的所有数据变更(主进程还在服务),并存储在 server.aof_rewrite_buf_blocks 中;后台子进程结束后,redis更新缓存追加到 AOF 文件中 缺点:AOF的文件过大,影响持久化性能。

 

Redis主从同步:

Redis的同步分为全部同步及部分同步。当我们redis slave客户端连接redis master的时候,master会判断slave是否为第一次同步.  redis master后面不单单是靠IP及PORT识别用户,更主要的是通过slave提交的runid标志来识别客户端。

如果判定为第一次同步,master首先会注册一个监听器,这个监听器主要用来收集用户的命令。这些命令放在一个叫积压空间的数据结构里,之后再fork一个子进程调用bgsave函数进行RDB快照。 fork子进程的速度是很快的,因为是cow 写实拷贝,意思就是说,子进程只是复制父进程的页表,然后物理内存页帧是一样的。 当父进程 或者 子进程要修改数据的时候,内核才会copy数据到独立的页帧。

如果判定为不是第一次同步,那么客户端会根据你的offset偏移量返回数据。  但是这里也是有个判断条件的,如果slave长期没有连接master,他的nextoffset在我的环形队列中早已经没有,那么你走的还是RDB得那个流程。 反之你的offset在我的记录中,那么我会返回命令可以。

假设你当前的偏移量为200, 如果主服务器又执行了命令 SET blog xiaorui.cc (协议格式的长度为 33 字节),将自己的复制偏移量更新到了 233 , 并尝试向从服务器c传播这条指令。但这条命令却因为网络故障而在传播的途中丢失, 那么主从服务器之间的复制偏移量就会出现不一致: 主服务器的复制偏移量会被更新为233 , 而从服务器的复制偏移量仍然为 200。

这nima主从之间的数据不同步怎么办?   数据不一致是个很场景的问题。

这问题不大,因为slave每次做同步的时候都会拿着自己上次的offset。 这时候master记着你是233,但这时候你居然提交了200.   那master会顺从slave的意思,你要啥就给你啥。  对于redis来说,你只要发送sync指令,我就可以根据你的offset状态来发送数据。

 

下面就是redis的Master Slave同步的流程图.

 

 

Redis集群

Redis 集群是一个可以在多个 Redis 节点之间进行数据共享的设施(installation)。Redis 集群不支持那些需要同时处理多个键的 Redis 命令, 因为执行这些命令需要在多个Redis 节点之间移动数据, 并且在高负载的情况下,这些命令将降低 Redis 集群的性能,并导致不可预测的行为。Redis 集群通过分区(partition)来提供一定程度的可用性(availability): 即使集群中有一部分节点失效或者无法进行通讯, 集群也可以继续处理命令请求。Redis 集群提供了以下两个好处:将数据自动切分(split)到多个节点的能力。当集群中的一部分节点失效或者无法进行通讯时, 仍然可以继续处理命令请求的能力。

1.Redis 集群使用数据分片(sharding)来实现官方的基于服务器的实现方案:一个Redis 集群包含 16384 个哈希槽(hash slot), 数据库中的每个键都属于这 16384 个哈希槽的其中一个,集群使用公式 CRC16(key) % 16384 来计算键 key 属于哪个槽, 其中 CRC16(key) 语句用于计算键 key 的 CRC16 校验和 。如

一个集群可以有三个节点, 其中:

节点 A 负责处理 0 号至 5500 号哈希槽。

节点 B 负责处理 5501 号至 11000 号哈希槽。

节点 C 负责处理 11001 号至 16384 号哈希槽。

如果用户将新节点 D 添加到集群中, 那么集群只需要将节点 A 、B 、 C 中的某些槽移动到节点 D 就可以了。

 

与此类似, 如果用户要从集群中移除节点 A ,那么集群只需要将节点 A 中的所有哈希槽移动到节点 B 和节点 C , 然后再移除空白(不包含任何哈希槽)的节点 A 就可以了。因为将一个哈希槽从一个节点移动到另一个节点不会造成节点阻塞,所以无论是添加新节点还是移除已存在节点, 又或者改变某个节点包含的哈希槽数量, 都不会造成集群下线。

         为了使得集群在一部分节点下线或者无法与集群的大多数(majority)节点进行通讯的情况下,仍然可以正常运作,Redis 集群对节点使用了主从复制功能。因为当某个节点下线了,部分哈希槽就没法处理了。

2. Redis Sharding(客服端分片)即用一致hash算法实现集群。

3. Redis代理中间件twemproxy完成sharding, twemproxy处于客户端和服务器的中间,将客户端发来的请求,进行一定的处理后(如sharding),再转发给后端真正的Redis服务器。也就是说,客户端不直接访问Redis服务器,而是通过twemproxy代理中间件间接访问。

增加代理中间件的Redis集群架构如下:

 

twemproxy中间件的内部处理是无状态的,它本身可以很轻松地集群,这样可避免单点压力或故障。

twemproxy又叫nutcracker,起源于twitter系统中redis/memcached集群开发实践,运行效果良好,后代码奉献给开源社区。其轻量高效,采用C语言开发,工程网址是:https://github.com/twitter/twemproxytwemproxy后端不仅支持redis, 同时也支持memcached,这是twitter系统具体环境造成的。由于使用了中间件,twemproxy可以通过共享与后端系统的连接,降低客户端直接连接后端服务器的连接数量。

同时,它也提供sharding功能,支持后端服务器集群水平扩展。统一运维管理也带来了方便。

 

 

 

Redis测试

Cpu : Inter(R) Core(TM)i3-4160  window10_64bit RAM(3.87G可用)

客服端:jedis 2.8.0   服务端:redis3.0(windows版本)

客户端 pom.xml如下:

   <dependency>

            <groupId>redis.clients</groupId>

            <artifactId>jedis</artifactId>

            <version>2.8.0</version>

       </dependency>

写入1000   167ms

写入10000  844ms

写入 100000 5082ms

写入 1000000 46183ms

  

 TestRedis.java

package test;

 

import java.util.Date;

 

import redis.clients.jedis.Jedis;

 

publicclass TestRedis {

   publicstaticvoid main(String[] args) {

     Order order = getOrder();

      RedisClient redisClient = new RedisClient();

     Jedis jedis = redisClient.getJedis();

     long start = System.currentTimeMillis();

     for(int i =1;i<=1000000;i++){

        jedis.set(String.valueOf(i).getBytes(),SerializationUtil.serialize(order));

     }

     long end = System.currentTimeMillis();

     System.out.println(end-start+"ms");

   }

  

  

   privatestatic Order getOrder(){

     Order order = new Order();

     order.setAgentBusiness("lp");

     order.setAgentId("880030");

     order.setAgentOrderNo("1250581407");

     order.setCityCode("1");

     order.setCreated(new Date());

     order.setOrderId("1250581407");

     order.setOrderStatus("64");

     order.setPhoneNo("18210933779");

     order.setProvinceCode("1");

     order.setRechargeType("1");

     order.setSkuId("1250581407");

     order.setSkuName("北京移动30");

     order.setVenderBusiness("lp");

     order.setVenderId("2290001");

     order.setVenderOrderNo("1250581407");

     order.setVenderSkuId("1250581407");

     return order;

   }

}

 

源码

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值