实现一个简易版的Redis服务器

本文介绍了如何实现一个简易版的Redis服务器,包括了解Redis的基本概念、遵循BSD协议、实现服务器功能的三个阶段(用户交互、并发用户、持久化)以及测试和难点分析。通过这个项目,作者掌握了Redis的数据类型和如何在Java中处理这些类型,实现了并发用户支持和数据持久化功能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

一、了解Redis

二、Redis服务器的编写

第一阶段:基本指令的实现

第二阶段:多用户并发

第三阶段:持久化

三、测试

四、拓展

五、收获

一、了解Redis

1、简介

Redis是一个开源的使用ANSI C语言编写、遵守BSD协议、支持网络、可基于内存亦可持久化的日志型、key-value数据库。它通常被称为数据结构服务器,因为值(value)可以是字符串(String),哈希(Hash),列表(List),集合(Set)和有序集合(sorted Set)等类型。

2、Redis的BSD协议

3、Redis的数据类型

Redis的数据结构  key-value

key为String

value为字符串(String),哈希(Hash),列表(List),集合(Set)和有序集合(sorted Set)等类型。

各个类型在java中以Map<key,value>存储形式如下:

二、实现Redis服务器

写在前面:功能展示        

1.思路:

  • 将字节流依照Redis的协议反列化为Java能够识别的对象,分析该对象的指令信息(例如是LPUSH、LPOP等),通过反射机制实例化处理该对象的类,在对应的实现类中完成指令 (例如实现将对象中的数据信息储存在内存中或者从内存中删除对应的信息等),把实现指令的结果依照协议序列化为字节流写入到Redis客户端。
  • 使用Socket套接字接受来自客户端的连接,利用固定线程池实现对多个用户的服务
  • 使用ObjectInputStream与ObjectOutputStream类对存在内存中的数据读写,实现所储存信息的持久化功能

 2.思路框图

3.将该服务器的功能分解三个阶段完成

第一二阶段以Command与Protocol为主,第三阶段以Permanent为主

第一阶段:一个用户与服务器的交互

 实现的过程:

  • 使用Socket套接字实现服务器与客户端的连接
  • 字节流对象遵循BSD协议规则反序列化为Java对象(Object)Protocol:ProtocolDecode:字节流-------->java对象
  • 确认对象类型并转换为所属类型,通过反射机制找到其相应的类
  • 该类从数据库(并非磁盘中的数据库,是存放数据的一个类)中得到数据
  • 在该类中对所收到的数据与数据库中的数据信息结合处理
  • 把处理完毕信息状态(Java对象)遵循BSD协议规则序列化为字节流响应给client

1.字节流对象遵循BSD协议规则反序列化为Java对象(Object)

  •    在实现这个功能的过程中,用到基本的I/0流处理操作、字符读取结束的判断等。
  •    对于Redis中Array、Bulk String类型格式的解析完全依赖于Simple String、Integer类型格式的解析。所以只要能够解析出Simple String和Integer,其他的几种类型就可以成功解析了。

1)对于Integer的解析很简单,只要读到的第一个字符是 ':',则可以断定client输入的为Redis中的Integer类型,即Java中的long类性数据,继续读取字符,直到读取到的字符为’\r‘时,对下一个读取的字符进行判断,如果是’\n‘,即该Integer类型解析完毕,可将其转换为Java中的long对象,否则所输入的类型格式无法解析成Java中的对象(抛出异常,两方不能友好交流)

2)对于Simple String的解析则较为复杂,循环读取字符,第一次读取字符时,如果读取到的字符位'\r'时,再次读取一次字符(该字符位‘\n’时跳出循环),如果不是'\r',每次循环中只读取一次字符

如果用户输入的是'+OK\r\n' ,则非常简单,以’+‘开始可以确定为Redis中的Simple String类型,读取结束的标志“\r\n”,对应Java中的String类型,

如果用户输入的是'+OK\r\r\n',如果继续按照对Simple String的解析方法,则“\r\r”都会被写入Java对象的真正内容中,造成错误,

所以如果第二次读取到的字符仍为‘\r’时,该‘\r’被写入到Java中的真正内容中去,并且作为下一次循环内容中的判断条件,下一次循环只会读取一次字符(因为只有在不是‘\r’的情况下才会读取一次) ,以这样的方式处理能够解决多个‘\r’存在的所有情况

3)对于Bulk String的解析,'$6\r\nfoobar\r\n' 或者 '$-1\r\n' ,以‘$’开始,‘$’后的字符作为输入的真正内容的长度,以“\r\n”作为结束标志,所以在Java中可以定义一个byte[长度],对于接下来的字节流的解析调用上述的Simple String解析方法即可

4)对与Array的解析,以‘*’开始,其后字符表示其数组长度,后面的字符全为对应的内容

2.确认对象类型并转换为所属类型,通过反射机制找到其相应的类

对输入对象类型的判断(期望为List),当对象为所期望的类型时,判断其长度是否符合

提取对象中的元素并为其转型,得到要操作的指令信息

使用Class.forName(),通过指令信息字符其对应的类

如果找到该类实例化类对象,并把处理过的对象传入到类对象中去,如果找不到,证明用户的输入存在问题,提示用户重新输入

3.在该类对象中处理指令,以LPUSHCommand所在的类对象为例说明:

        首先先判断所传送过来的参数的长度所传送过来的数据累类型必须是列表),在满足长度要求的前提下,从参数中得到对应的key值,value值,再次以该key值为参数,从database中获取该key值所对应的列表,如果列表为空,则数据库中的<String,ArrayList<>> 对应的变量将会把(key,new ArrayList<>)插入进去,并返回key所对应的空列表,在LPUSHComand中,把value值头插入列表中;如果列表不为空,则直接返回key所对应的列表信息,并把value值头插入该列表中。至此,Lpush指令实现成功。如果不满足长度,则向用户反应数据格式的信息

4.指令执行成功与否,都要向用户反馈,成功,告知用户该操作成功,否则告诉用户输入存在问题。这个就涉及到将Java中的对象信息遵循BSD规则编码为redis识别的字节流信息。在解析字节流信息时,给字节流信息拆包装,现在给Java对象封标签,编码为Redis类型所对应的格式。

1)对于Java中的String类型,转换为Redis中的Simple String就是将其转换为字节数组,并在其首部加上‘$’符,在其末尾加上'\r''\n'。(但是这个不太用,不用返回OK)

2)对于Java中的String类型,转换为BulkString就是将其转换为字节数组,计算其长度,并在其首部加上‘:’符,在其末尾加上'\r''\n'。把转换后的字节数组写入,再其末尾添加"\r\n"

3)对于想要返回给用户错误信息的,可以使用Redis中的error格式,在其首部加上‘-’,在其末尾加上'\r''\n'

4)对于Java中的long或者int类型的,就是将其先变为String.valueOf(整数),在将其转换为字节数组形式写入,在其首部加上‘:’,在其末尾加上'\r''\n'

5)对于Java中的列表类型,将其长度写入,其首部加上‘*’,在其末尾加上'\r''\n',再遍历该数组,判断列表中元素的类型,如果是long或者int使用上述方法 4)将其写入,如String类型,使用方法 2)将其写入,如果是列表,使用方式 5)本身

Java中的异常对应与Redis中的error,但是对于这个规则,我没有遵守,在处理转换成功的Java对象时,如果有条件不满足,我就会使用Redis中error对应的格式将提示信息编码,告知用户输入有问题,对于该项目中的异常,我并没有使用error格式编码反馈给用户

 

第二阶段:并发用户的实现

使用线程池来处理多个用户请求。每连接到一个用户,就会有一个线程去执行任务。在我写的服务器上把ServerSocker的端口号固定为Redis的端口号,下载一个redis-2.4.5-win32-win64,使用该客户端连接我的服务器,检测各个功仍然能正常实现。

如果客户端不想要输入信息,直接输入“exit”或者“quit”即可自动关闭窗口,还可以输入“quit”使程序阻塞,无法继续执行下去,我使用QUIZCommand继承Command,在类中,检测到quiz时,调用System.exit(),程序阻塞

第三阶段:持久化的实现

将数据库中存在的信息写入到磁盘中,使得信息能够持久化。创建一个Permament类来专门管理数据加载与存储过程

该类以单例模式创建,只能被创建一次。

使用ObjectInputStream和ObjectOutputStream来读写数据库中的信息

当向内存中插入/更新数据/删除数据时,先把数据插入到database中,从database中获取信息再将其写入到文件中

当启动服务器时,先从文件中加载信息到database中

使用ObjectInputStream和ObjectOutputStream都是在try的方括号里面写的,因为在处理完之后,可以自动释放资源。不用手动关闭。

三、测试

1.功能测试

  • 创建一个maven项目,导入junit4.12和Jedis 2.9.0依赖
  • 编写服务器各功能的测试脚本(主要以边界值和等价类的测试用例编写)
  • 启动Redis服务器,执行测试脚本

测试点如下:

对 lpush key value 指令测试,正常输入时,返回(Integer)列表中数据个数 

对 lrange key index1  index2 指令测试,(数组下标)正常输入时,返回(Integer)列表中数据

    当index2 超过数组下标时,返回从index1开始到列表结束的数据

    当index1或者index2为负时,返回倒着数的小标的数据

对 pop key  指令测试,正常输入时,返回(Integer)列表第一个数据,并且database中的该列表中的数被删除

其他功能的测试如上,hset hget sadd 正常

当所输入的字符不是正确指令时,返回一个友好提示

指令输入正常的情况下,如果未存在数据则会抛出异常,使用Jedis客户端测试会出现以下异常,使用Redis客户端测试则会阻塞

redis.clients.jedis.exceptions.JedisConnectionException: java.net.SocketTimeoutException: Read timed out

2.性能测试

1)并发用户的测试

线程池的大小设置为20000,导入Jedis 2.9.0依赖包 ,使用jedis 类创建一个客户端,循环18000次,相当与创建18000个客户,该服务器能够正常响应,当线程池的大小为2000以上时,本机蓝屏,散热器噪声超大,估计是支持不了了,最多20000个用户

2)性能测试

该功能主要是对服务器响应速度的测试。

未持久化时:3000条数据耗时约932ms,写入速度  约等于3000条/s  读取速度  1000条数据耗时369ms 读取速度约为 3000条/s

 持久化时:1000条数据180s,写入速度5.5条/秒  读取速度

总结:该服务器能够实现Redis的基本LPUSH LPOP LRANGE SADD HSET HGET等基本功能,响应速度能够基本满足用户需求

四、难点

任何一个项目 ,只要能够搭清楚框架,实现起来就会比较简单。这个项目一开始,我的思路还是非常清楚的,但是在Command包下实现类中的方法时,我就有点懵逼了,我愣是没弄清楚参数需要转换的类型,以及参数处理完成后的反馈,服务器输出流这个参数我竟然传到解码的类中了。

我在想实现数据的持久化时应该用文件还是数据库,鉴于我之前的一个项目用的是数据库,所以这个项目我选择了文件,不过用数据库实现也挺容易的,有五种数据类型,那我就建五张表,对于hash类型,我就可以至少有三个字段,存储key,field,value

多用户这一方面我做的不好,我只是实现能够连接到多个用户,但是会不会产生数据信息脏读这一方面我还正在思考去解决

五、收获

Redis数据结构类型丰富,如果开发一个简易版的博客系统,可以将数据存储在这个服务器上。文章用hash结构存储,文章列表用ZSet有序集合存储,用列表实现分页功能。借助于这个服务器,就可以自己搭建一个博客系统,想想就开心


源代码:https://github.com/pwby/LikeRedis

  

 

 

                

                                                            

                                                                               

                                                                                 

 

                                                       

 

 

                                                                                                             

 

 

 

 

 

 

 

                 

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值