目录
统一硬盘操作
引言
问题:
- 关于 Message(消息)为啥在硬盘上存储?
回答:
- 消息操作并不涉及到复杂的增删查改
- 消息数量可能会非常多,使用数据库的访问效率是并不高
- 因此我们不使用数据库进行存储,而是直接将消息存储到文件中
约定存储方式
- 此处设定了消息具体如何在文件中存储
约定一
- 消息依附于队列,因此在存储时,我们可以将消息按照 队列 纬度展开
- 之前我们因为引入 SQLite 已经设置了一个 data 目录(meta.db 就在该目录中)
- 所以我们可以在现有的 data 目录中存储一些子目录
- 每个子目录对应一个队列, 即 子目录名 就是 队列名
- 约定目录结构如上图所示
- 文件 queue_data.txt 保存消息的内容
- 文件 queue_stat.txt 保存消息的统计信息
// 约定消息文件所在的目录和文件名 // 这个方法,用来获取到指定队列对应的消息文件所在路径 private String getQueueDir(String queueName) { return "./data/" + queueName; } // 这个方法用来获取该队列的消息数据文件路径 // 二进制文件使用 txt 作为后缀,不太合适,txt 一般表示文本,此处我们也就不改了 // .bin / .dat private String getQueueDataPath(String queueName) { return getQueueDir(queueName) + "/queue_data.txt"; } // 这个方法用来获取该队列的消息统计文件路径 private String getQueueStatPath(String queueName) { return getQueueDir(queueName) + "/queue_stat.txt"; } // 创建队列对应的文件和目录 public void createQueueFiles(String queueName) throws IOException{ // 1、先创建队列对应的消息目录 File baseDir = new File(getQueueDir(queueName)); if(!baseDir.exists()) { // 不存在,就创建这个目录 boolean ok = baseDir.mkdirs(); if(!ok) { throw new IOException("创建目录失败!baseDir = " + baseDir.getAbsolutePath()); } } // 2、创建队列数据文件 File queueDataFile = new File(getQueueDataPath(queueName)); if(!queueDataFile.exists()){ boolean ok = queueDataFile.createNewFile(); if(!ok) { throw new IOException("创建文件失败!queueDataFile = " + queueDataFile.getAbsolutePath()); } } // 3、创建消息统计文件 File queueStatFile = new File(getQueueStatPath(queueName)); if(!queueStatFile.exists()){ boolean ok = queueStatFile.createNewFile(); if(!ok) { throw new IOException("创建文件失败!queueStatFile = " + queueStatFile.getAbsolutePath()); } } // 4、给消息统计文件,设定初始值 0\t0 Stat stat = new Stat(); stat.totalCont = 0; stat.validCount = 0; writeStat(queueName,stat); } // 删除队列的目录和文件 // 队列也是可以被删除的,当队列删除之后,对应的消息文件啥的,自然也要随之删除 public void destroyQueueFiles(String queueName) throws IOException{ // 先删除里面的文件,在删除目录 File queueDataFile = new File(getQueueDataPath(queueName)); boolean ok1 = queueDataFile.delete(); File queueStatFile = new File(getQueueStatPath(queueName)); boolean ok2 = queueStatFile.delete(); File baseDir = new File(getQueueDir(queueName)); boolean ok3 = baseDir.delete(); if(!ok1 || !ok2 || !ok3) { // 有任意一个删除失败,都算整体删除 throw new IOException("删除队列目录和文件失败! baseDir = " + baseDir.getAbsolutePath()); } } // 检查队列的目录和文件是否存在 // 比如后续有生产者给 broker server 生产消息了,这个消息就可能需要记录到文件上(取决于消息是否要持久化) public boolean checkFilesExits(String queueName) { // 判定队列的数据文件 和统计文件是否都存在!! File queueDataFile = new File(getQueueDataPath(queueName)); if(!queueDataFile.exists()) { return false; } File queueStatFile = new File(getQueueStatPath(queueName)); if(!queueStatFile.exists()) { return false; } return true; }
约定二
- queue_data 是一个二进制格式的文件
- 该文件中包含若干个消息,每个消息均以二进制的方式存储
- 约定每个消息的组成部分如上图所示
消息序列化
- 序列化就是将一个对象(结构化的数据)转成一个 字符串 或 字节数组
注意点一:
- 序列化完成之后,对象的信息不丢失
- 因此在后面进行反序列化操作时,才能将序列化的 字符串 或 字节数组 重新转化成对象
注意点二:
- 将对象序列化后,更方便存储和传输
- 存储:一般存储在文件中,文件只能存 字符串/二进制数据,不能直接存对象
- 传输:通过网络传输
JSON 格式
- 在 Java 中,Jackson 是一个流行的 JSON 处理库,它提供了 ObjectMapper 类来处理 JSON 数据的序列化和反序列化
问题:
- Message 中存储的 body 部分为二进制数据,可以用 JSON 进行序列化吗?
回答:
- JSON 格式通常用于标识文本数据,而无法直接存储二进制数据
- JSON 格式中包含一些特殊符号( : " { } ),如果直接存储二进制数据,可能会受到这些特殊符号的影响,导致 JSON 解析错误
具体理解:
- 如果存 文本数据,你的键值对中不会包含上述特殊符号
- 如果存 二进制数据,且万一某一二进制的字节正好就与上述特殊符号的 ASCII 一样,此时便可能会引起 JSON 解析格式错误
解决方案A
- 针对二进制数据进行 Base64 编码,将其转化为文本数据,然后再存储在 JSON 格式中
注意点一:
- Base64 将每 3 个字节的二进制数据转换为 4 个文本字符,从而确保所有字符都是文本字符,避免了特殊符号的问题(相当于是把二进制数据转成文本了)
- 比如在 HTML 中嵌入一个图片,图片其本身为二进制数据,此时便可以将图片的二进制 数据进行 Base64 编码,然后便可以把图片直接以文本的形式嵌入到 HTML 中
注意点二:
- Base64 这种方案,效率低,且伴随有额外转码开销,同时,还会使数据变得更大
解决方案B
- 放弃使用 JSON 格式,直接使用二进制的序列化方式,针对 Message 对象进行序列化
注意点一:
- 针对二进制序列化,有很多种解决方案
- Java 标准库提供了序列化的方案 ObjectInputStream 和 ObjectOutputStream
- Hessian
- protobuffer
- thrift
注意点二:
- 我们将直接使用 标准库自带的序列化方案
- 该方案最大的好处就是 不必引入额外的依赖
import java.io.*; //下列的逻辑,不仅仅是 Message,其他的 Java 中的对象,也是可以通过这样的逻辑进行序列化和反序列化的 //如果要想让这个对象能够序列化或者反序列化,需要让这个类能够实现 Serializable 接口 public class BinaryTool { // 把一个对象序列化成一个字节数组 public static byte[] toBytes(Object object) throws IOException { // 这个流对象相当于一个边长的字节数组 // 就可以把 object 序列化的数据给逐渐写入到 byteArrayOutputStream 中,再同一转成 byte[] try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()){ try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream)){ // 此处的 writerObject 就会把对象进行序列化,生成的二进制字节数据,就会写入到 // ObjectOutputStream 中 // 由于 ObjectOutputStream 有关联到了 ByteArrayOutputStream,最终结果就写入到 ByteArrayOutputStream 里了 objectOutputStream.writeObject(object); } // 这个操作就是把 byteArrayOutputStream 中持有的二进制数据取出来,转成 byte[] return byteArrayOutputStream.toByteArray(); } } // 把一个数组反序列化成一个对象 public static Object fromBytes(byte[] data) throws IOException,ClassNotFoundException{ Object object = null; try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(data)){ try (ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream)){ // 此处的 readObject 就是从 data 这个 byte[] 中读取数据并进行反序列化