Skip to content

Latest commit

 

History

History
192 lines (133 loc) · 11.3 KB

File metadata and controls

192 lines (133 loc) · 11.3 KB

本课我们来简单介绍一下 Redis 服务端和客户端的原理,因为一般面试的时候会问起,我们自己要做到对整体有个了解,做到心中有数。

一 Redis 服务器端原理

Redis Server 的实现原理关键内容介绍。

1. Redis 内部储存的核心对象 RedisObject

redisObject

Redis 内部使用一个 redisObject 对象来表示所有的 key 和 value,redisObject 最主要的信息如图所示:

  • type 代表一个 value 对象具体是何种数据类型。
  • encoding 是不同数据类型在 Redis 内部的存储方式。
  • vm 字段,只有打开了 Redis 的虚拟内存功能,此字段才会真正的分配内存,该功能默认是关闭状态的。

下图展示了 redisObject 、Redis 所有数据类型以及 Redis 所有编码方式(底层实现)三者之间的关系:

redisObject 数据结构关系图

2. 数据类型介绍

  • String(字符串类型的 Value)

String,可以 String 字符串,也可是是任意的 byte[] 类型的数组,如图片等。String 在 Redis 内部存储默认就是一个字符串,此时 redisObject 的 type=string,value 存储的是一个普通字符串,那么对应的 encoding 可以是 raw 或者是 int,如果是 int 则代表实际 Redis 内部是按数值型类存储和表示这个字符串的,当然前提是这个字符串本身可以用数值表示,比如:"123" "456"这样的字符串。而当遇到 incr、decr 等操作时会转成数值型进行计算,此时 redisObject 的 encoding 字段为 int。

  • List(List 类型的 Value)

Redis list 的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销,Redis 内部的很多实现,包括发送缓冲队列等也都是用的这个数据结构。而此时 redisObject 的 type 属性为REDIS_LIST,encoding 属性为REDIS_ENCODING_LINKEDLIST,它的值保存在一个双端链表内,而 ptr 指针就指向这个双端链表。

  • Hash(连表结构)

Redis Hash 对应 Value 内部实际就是一个类似 HashMap 的数据结构,实际上 Hash 的成员比较少时 Redis 为了节省内存会采用类似一维数组的方式来紧凑存储,而不会采用真正的 HashMap 结构,而是使用 ZIPLIST。

当使用REDIS_ENCODING_ZIPLIST编码哈希表时,程序通过将键和值一同推入压缩列表,从而形成保存哈希表所需的键-值对结构:

REDIS_ENCODING_ZIPLIST

新添加的 key-value 对会被添加到压缩列表的表尾。

此时 redisObject 的 type 属性为 REDIS_HASH , encoding 属性为 REDIS_ENCODING_ZIPLIST ,那么这个对象就是一个 Redis 哈希表,它的值保存在一个 zipList 里,而 ptr 指针就指向这个 zipList ;诸如此类。

  • Set/Sorted Set

set 的内部实现是一个类 Hash 和跳跃表(SkipList)来保证数据的存储和有序,实际就是通过计算 hash 的方式来快速排重的,这也是 set 能提供判断一个成员是否在集合内的原因。

如果大家对数据结构理解不够深入的话,可以出门右拐,看我的另外一篇 chat <如何理解并掌握 Java 数据结构 >

3. Redis的持久化机制

Redis 由于支持非常丰富的内存数据结构类型,如何把这些复杂的内存组织方式持久化到磁盘上是一个难题,所以 Redis 的持久化方式与传统数据库的方式有比较多的差别,Redis 一共支持的持久化方式,分别是:

  • 定时快照方式(snapshot)
  • 基于语句追加文件的方式(aof)
1)定时快照方式(snapshot)

该持久化方式实际是在 Redis 内部一个定时器事件,每隔固定时间去检查当前数据发生的改变次数与时间是否满足配置的持久化触发的条件,如果满足则通过操作系统 fork 调用来创建出一个子进程,这个子进程默认会与父进程共享相同的地址空间,这时就可以通过子进程来遍历整个内存来进行存储操作,而主进程则仍然可以提供服务,当有写入时由操作系统按照内存页(page)为单位来进行 copy-on-write 保证父子进程之间不会互相影响。

该持久化的主要缺点是定时快照只是代表一段时间内的内存映像,所以系统重启会丢失上次快照与重启之间所有的数据。

2)基于语句追加方式(aof)

aof 方式实际类似 MySQL 的基于语句的 binlog 方式,即每条会使 Redis 内存数据发生改变的命令都会追加到一个 log 文件中,也就是说这个 log 文件就是 Redis 的持久化数据。

aof 的方式的主要缺点是追加 log 文件可能导致体积过大,当系统重启恢复数据时如果是 aof 的方式则加载数据会非常慢,几十G的数据可能需要几小时才能加载完,当然这个耗时并不是因为磁盘文件读取速度慢,而是由于读取的所有命令都要在内存中执行一遍。另外由于每条命令都要写 log,所以使用 aof 的方式,Redis 的读写性能也会有所下降。

4.Redis 主从复制原理

redis主从复制原理图

(1)当启动一个 Slave 进程后,它会向 Master 发送一个 SYNC Command,请求同步连接。无论是第一次连接还是重新连接,Master 都会启动一个后台进程,将数据快照保存到数据文件中,同时 Master 会记录所有修改数据的命令并缓存在数据文件中。

(2)后台进程完成缓存操作后,Master 就发送数据文件(dump.rdb)给 Slave,Slave 端将数据文件保存到硬盘上,然后将其在加载到内存中,接着 Master 就会所有修改数据的操作,将其发送给 Slave 端。

(3)若 Slave 出现故障导致宕机,恢复正常后会自动重新连接,Master 收到 Slave 的连接后,将其完整的数据文件发送给 Slave,如果 Mater 同时收到多个 Slave 发来的同步请求,Master 只会在后台启动一个进程保存数据文件,然后将其发送给所有的 Slave,确保 Slave 正常。

Redis 复制工作原理
  • 如果设置了一个 Slave,无论是第一次连接还是重连到 Master,它都会发出一个 SYNC 命令;
  • 当 Master 收到 SYNC 命令之后,会做两件事:
    • a) Master 执行 BGSAVE,即在后台保存数据到磁盘(rdb 快照文件);
    • b) Master 同时将新收到的写入和修改数据集的命令存入缓冲区(非查询类);
  • 当 Master 在后台把数据保存到快照文件完成之后,Master 会把这个快照文件传送给 Slave,而 Slave 则把内存清空后,加载该文件到内存中;
  • 而 Master 也会把此前收集到缓冲区中的命令,通过 Reids 命令协议形式转发给 Slave,Slave 执行这些命令,实现和 Master 的同步;
  • Master/Slave 此后会不断通过异步方式进行命令的同步,达到最终数据的同步一致。

如果细节有出入,需要注意版本,但是大体的结构和思路是不变的。如果大家相对 Server 原理了解更多,推荐书籍 Redis 的设计与原理

二 RedisTemplate 原理

1. Spring Data Redis 简单源码解读

  • spring-data-redis 提供了 Redis 操作的封装和实现;
  • RedisTemplate 模板类封装了 Redis 连接池管理的逻辑,业务代码无须关心获取,释放连接逻辑;
  • Spring Redis 同时支持了 Jedis、Jredis、rjc 客户端操作。

Spring Redis 源码设计逻辑可以分为以下几个方面。

  • Redis 连接管理:封装了 Jedis、Jredis、Rjc 等不同 Redis 客户端连接;
  • Redis 操作封装:value、list、set、sortset、hash 划分为不同操作;
  • Redis 序列化:能够以插件的形式配置想要的序列化实现;
  • Redis 操作模板化:Redis 操作过程分为获取连接、业务操作、释放连接,模板方法使得业务代码只需要关心业务操作;
  • Redis 事务模块:在同一个回话中,采用同一个 Redis 连接完成。

spring data redis 源码

2. 实例解读

我们以下面操作解读一下源码实现过程。

(1)redisTemplate 方法:

redisTemplate.opsForValue().set("key","value");

(2)调用到的 ValueOperations 源码实现:

class DefaultValueOperations<K, V> extends AbstractOperations<K, V> implements ValueOperations<K, V> {
   DefaultValueOperations(RedisTemplate<K, V> template) {
      super(template);
   }
public void set(K key, V value) {
   final byte[] rawValue = rawValue(value);
   execute(new ValueDeserializingRedisCallback(key) {
      protected byte[] inRedis(byte[] rawKey, RedisConnection connection) {
         connection.set(rawKey, rawValue);
         return null;
      }
   }, true);
}
......}

(3)找到 RedisConnection 的实现类,我们查看 RedisAutoConfiguration 可以发现是 JedisConnectionFactory:

@ConditionalOnClass({JedisConnection.class, RedisOperations.class, Jedis.class})
@EnableConfigurationProperties({RedisProperties.class})
public class RedisAutoConfiguration{
......
@ConditionalOnMissingBean({RedisConnectionFactory.class})
public JedisConnectionFactory redisConnectionFactory() throws UnknownHostException {
    return this.applyProperties(this.createJedisConnectionFactory());
}
......//
}

(4)打开 JedisConnection 分析关键代码如下:

package org.springframework.data.redis.connection.jedis;
public class JedisConnection extends AbstractRedisConnection {
public void set(byte[] key, byte[] value) {
   try {
      if (isPipelined()) {
         pipeline(new JedisStatusResult(pipeline.set(key, value)));
         return;
      }
      if (isQueueing()) {
         transaction(new JedisStatusResult(transaction.set(key, value)));
         return;
      }
      jedis.set(key, value);//后面以这里为例,调用jedis的代码
   } catch (Exception ex) {
      throw convertJedisAccessException(ex);
   }
}
...... //此类里面就会有很多
}

(5)后面的方法的调用就会到 Jedis 包里面的东西了:

package redis.clients.jedis;
public class BinaryJedis implements BasicCommands, BinaryJedisCommands, MultiKeyBinaryCommands, AdvancedBinaryJedisCommands, BinaryScriptingCommands, Closeable {
public String set(byte[] key, byte[] value) {
    this.checkIsInMultiOrPipeline();
    this.client.set(key, value);
    return this.client.getStatusCodeReply();
}
......//this.client.set 就是jedis里面的对socket连接的redis-client的命令的发送。
}

(6)到此,其实我们就可以明白了整个 Spring Data Redis 的完美封装,再去查看 jedis 的源码的时候你就会发现,其实后面就是建立 Socket 连接,然后发送一些 Redis-client 里面的命令到 Redis-server 的监听端口。

RedisTemplate 的详细使用方法大家可以关注作者的 chat,后续会有个 chat 详细介绍。