手写Redis服务端,从设计者的角度聊一聊Redis本身
【文章来源】https://sourl.cn/mqFYeU
零,起因
我为什么要造redis这个轮子?1 破除对redis神秘感。2 “基础服务中台”的同事们在开会讨论redis云,以及redis代理。3 开一个redis资源并不是容易事,为什么不可以不可以写成java直接推送到未来云上,简单方便。
以这个思路我开始使用业余时间研究了redis的tcp通讯原理与redis命令,出发点是写一个redis云代理之类的云管理软件,但是还是忍不住写成了java版的redis,本文章主要分享redis的编写心路历程。
redis通讯 与 Netty
tcp
连到Redis服务器的客户端建立了一个到6379端口的TCP连接。
虽然RESP在技术上不特定于TCP,但是在Redis的上下文中,该协议仅用于TCP连接(或类似的面向流的连接,如unix套接字)。
使用netty作为通讯框架。
协议
Redis客户端和服务器端通信使用名为 RESP (REdis Serialization Protocol) 的协议。虽然这个协议是专门为Redis设计的,它也可以用在其它 client-server 通信模式的软件上。
RESP 协议在Redis1.2被引入,直到Redis2.0才成为和Redis服务器通信的标准。这个协议需要在你的Redis客户端实现。
RESP 是一个支持多种数据类型的序列化协议:简单字符串(Simple Strings),错误( Errors),整型( Integers), 大容量字符串(Bulk Strings)和数组(Arrays)。
RESP在Redis中作为一个请求-响应协议以如下方式使用:
客户端以大容量字符串RESP数组的方式发送命令给服务器端。服务器端根据命令的具体实现返回某一种RESP数据类型。在 RESP 中,数据的类型依赖于首字节:
单行字符串(Simple Strings):响应的首字节是 "+" 错误(Errors):响应的首字节是 "-" 整型(Integers):响应的首字节是 ":" 多行字符串(Bulk Strings):响应的首字节是"$" 数组(Arrays):响应的首字节是 "*"
另外,RESP可以使用大容量字符串或者数组类型的特殊变量表示空值,下面会具体解释。RESP协议的不同部分总是以 "\r\n" (CRLF) 结束。字符串 "foobar" 编码如下:
"$6\r\nfoobar\r\n"
实际redis命令是什么样的,比如 SET lhjljh lhjkjhkh
*3\r\n$3\r\nSET\r\n$6\r\nlhjljh\r\n$8\r\nlhjkjhkh
编解码
由于RESP天然是面向处理命令的,所以没办法直接把redis消息像grpc或者dubbo那样直接序列化和反序列化消息。
并且每个内容限定了长度,很适合做成及时序列化、零拷贝,直接针对输入流做反序列化和序列化,这一点与Protostuff序列化协议的设计很类似。所以序列化直接将服务端接收的流直接转成值。
编解码的实体类直接加入redis server 的处理某一个长连接tcp客户端的管道上。
命令处理
将消息解码成RESP,还需要将RESP转为Command对象,这里因为是java语言,方法与类绑定,编写上和理解上会更加容易。但是会增加一些开销。
redis 的数据结构
底层主结构
底层主树使用跳表ConcurrentSkipListMap实现,没用hash类map的原因是服务端是集群后,客户端可能使用hash路由,会导致服务端严重的hash冲突,性能大打折扣。
key为封装的“String”,重写了equals方法避免相同的key但是在jvm中指针不同。
value是一个接口,实现类是redis的五大基本类型,所有数据类型都包含超时时间。
key
用封装的值做value的原因是方便统一管理
list
底层使用LinkedList的原因是LinkedList实现了多种接口,实现各种命令直接调用其现成实现的方法即可
set
底层使用HashSet,redis里的set没有多特殊。
hash
底层使用HashMap,这里和开头说的HashMap不冲突。
为什么不用跳表?
压缩列表很巧妙,大抵的意思就是将通信收到的数组直接填充到list中,将list直接按照次序直接当map使用,主要是0拷贝的思想,无需创建新资源,性能极高,但注意压缩列表与压缩无关。
zset
首先需要封装一个带有值和分值的对象
再用TreeMap重写compare方法即可,使用TreeMap原因是他天然有良好的排序功能,很多hash一致路由的算法都用的TreeMap二开。
redis AOF 持久化
aof线程与tcp线程解耦,即写缓冲
在解析redis命令时,将redis写命令添加到写aof日志的队列中
这里自己封装了一个堵塞队列,单线程吞吐量可以达到3000W /s是LinkedBlockingQueue的6到10倍,完全可以胜任此场景
RingBlockingQueue吞吐量非常高的原因是使用了内存连续页的机制。
aof持久化协议
aof协议一句话概括就是将写命令,追加到日志中,开始时将命令读取,当作收到网络的命令执行即可。由于协议过于简单,这里就不贴链接了。aof之日格式如下图:
aof的加载与存储实现
这里读写内存都是用的内存文件映射,好处是读写性能好,坏处是可能会出现内存泄漏,调试期间比较麻烦。
内存文件映射与面向对象
这里存储和加载aof文件的代码都是面向过程的,看起来非常复杂。实际上之前是按照面向对象写的,封装成了行对象,调用落盘符和拾起方法就可以写入和读取aof中的命令,但是TPS仅为10w/s,后来权衡后改为面向过程,吞吐量提升到了100W的TPS以上。
redis 的集群特性
主从
这里很容易联想到mysql的只从,很多场景下会使用基于mysql主从的读写分离,或者zk的主从。
但实际上redis的主从是不保证一致性的,个人认为redist的主从主要考虑的是cap的分布式容错性。因为redis主从不保证一致性,所以使用redis读写分离,可能造成一些不一致的问题,写写是一致的,但是读是不一致的,可以根据项目需要做取舍。
主从复制
redis的主从复制这里作者没看懂(可能也是一致性上有坑没动力去看),所以没写出来。
分片集群
redis集群主要分为几个唯独:主从、分区集群、代理。一般在redis客户端的视角下,主要是分区集群,根据发送给redis的key做hash、md5等操作,取一个所有客户端的共识值,将key和value发送,也就是客户端路由分布式软件的集群实现方式京东的redis集群设计到redis具体一个分片。
redis 的压测与调优
aof内存泄漏
开启aof压测发现出现了内存泄漏,后来发现是频繁新建内存池而造成的,所以将内存池池化,即aof对象中仅存在一个bytebuff内存池。
内存复用提升性能
这里编解码没有单独开辟byte数据接收bytebuff的数据进行编解码,编解码直接读取bytebuff进行编解码,没有出现内存拷贝,唯独新建了BytesWrapper对象,但存储的数据都是使用BytesWrapper对象,对内存新建/销毁的开销很少。
0.05%消息延迟超200ms排查
下图为c语言版的redis压测数据:
下图为java语言版的redis压测数据:
发现java版的redis会出现小概率消息延迟,为什么那?
性能表现
redis原版的性能大概是E5系列CPU 4-5w左右,上图中是使用amd芯片测试的数据。使用redis自带的压测工具,维持100个客户端连接,java版性能是c语言原版性能的75-90%左右,性能依然强悍。
手写Redis服务端,从设计者的角度聊一聊Redis本身相关推荐
- 一个命令让redis服务端所有信息无所遁形~(收藏吃灰系列)
1.info命令作用 在redis客户端执行INFO 命令以便于计算机解析和人工阅读的简单格式返回有关redis服务端的所有信息和统计数据. 可选参数可用于选择特定的信息部分: Server 服务器基 ...
- redis服务端启动报错oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
启动redis服务端的时候报错了,报错截图如下 报错的倒数第二行写的address already in use 看起来就是服务已经被启动了,这时候尝试启动客户端试试,发现连接成功,说明确实已经有服务 ...
- 大话手游原始服务端搭建教程Centos
大话手游原始服务端搭建教程Centos 大家好,我是艾西,今天给大家分享一款回合制的ARPG大话手游搭建教程.游戏场景.精美的画面以及多元的人物做的非常棒.在游戏中可以穿越神话世界,同时也可以结交好友 ...
- Java中使用Jedis连接Redis服务端时提示:JedisConnectionException: Failed connecting
场景 Centos中Redis的下载编译与安装(超详细): https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/103967334 Re ...
- redis服务端的maxclient和最大连接空闲时间设置
1.redis服务器可以配置最大允许的客户端连接数,当大于设置的值时,连接被拒绝,可以通过cluster info clients查看拒绝连接数,占用最大输入缓冲区的客户端连接,占用最大输出缓冲区的客 ...
- redis服务端基本命令
服务端命令: redis 一,string: 1,set name value 2,setnx name value: 如果已经存在的值 不进行操作 不覆盖 3,setex name time va ...
- Netty傻瓜教程(一):Netty初探,只写个服务端也能工作
2019独角兽企业重金招聘Python工程师标准>>> 霸王硬上,直接开始,服务端: package com.hengzenc.NettyServer;import io.netty ...
- 服务器客户端通信协议,Redis服务端-客户端通信协议
了解Redis通信内容 Redis我们都比较熟悉,可以用来做缓存.分布式锁等,但是,其中的客户端与服务端是如何进行通信的呢? 我们可以分别模拟一个服务端或者客户端,打印查看来自实际连接的请求来获取它们 ...
- PHP写webservice服务端
1) WebService技术介绍 WebService是一种跨编程语言和跨操作系统平台的远程调用技术.仅仅有通过Web Service,client和server才可以自由的用HTTP进行通信.不论 ...
最新文章
- 记一次“曲折“的MySQL数据导出
- Spring中资源的加载ResourceLoader
- 【NOI2019】弹跳【二维线段树】【dijkstra】
- 多种时间格式字符串转换为Date对象
- php tp 模板assign,thinkphp中怎么让assign在另一个模板里使用呢?
- java正则表达 w,超强变态的正则(\w)((?=\1\1\1)(\1))+讲解
- 大android 平板,15.6英寸安卓大平板? 阿芙罗S1评测
- Mac/Linux/Win使用scrcpy投屏
- 使用脚本解决fstab挂载失败不能正常启动问题
- 【WEB搜索技术】课程学习大纲与学习感悟
- 【迁移学习】Self Paced Adversarial Training for Multimodal Few-shot Learning论文解读
- android 图片存取方法,6种备份Android照片的方法
- 【Vue基础七】--- 组件和模块概念
- 【数据可视化】数据之美---揭密优雅的数据解决方案背后的故事
- ez_website
- Android Paint、Canvas、Matrix使用讲解(一、Paint)
- 记忆犹新的定时任务的bug
- 计算机博士与管理科学与工程博士,南昌大学《管理科学与工程》专业博士研究生培养方案...
- 区块链到底能做什么?这506个备案项目告诉你
- 未授权和敏感文件泄露
热门文章
- matlab的帮助命令是英文的,4 Matlab 帮助系统
- Python将所有的英文单词首字母变成大写
- 51单片机程序及调试步骤实战经验
- LightOJ1245-Harmonic Number (II) 【数学调和级数】
- android系统用在什么电脑,手机变电脑的简单操作 适用于所有Android系统的向日葵...
- android 弹幕时间戳,【存档】B站直播数据包分析连载(2018-12-11更新/2020-04-12废止)...
- 南华大学c语言多少分才能过_成人高考难吗?多少分可以过?
- C++ queue 详细介绍
- 蓝桥杯-本质上升序列-java
- cefsharp 发送请求服务器_WEB服务器之HTTP协议