一条修改操作如何被处理
一条日志, OpLog,我们不应该包含复杂的 POI 对象,是一个极简的声明式的 JSON 结构。仅需描述三件事:谁、在哪、做了什么修改
{
"excelId": "excel_1024",
"logId": 15023,
"version": 12,
"userId": "user_abc",
"timestamp": 1718027127000,
"opType": "UPDATE_CELL",
"payload": {
"sheetName": "Sheet1",
"row": 4,
"col": 2,
"cellType": "STRING",
"oldValue": "Hello",
"newValue": "Hello World"
}
}
对前端而言,收到 WebSocket 的广播的这条 JSON 之后,直接找到对应的单元格,把内容刷新成对应的内容。前端并不需要知道后端时用 POI 还是别的东西存文件;
对于后端而言,当异步刷新快照的线程拿到该条日志 JSON 时,只需要与 POI 的操作对象作 API 的一系列处理。
冷热日志与快照闭环
热日志支持高并发和实时广播的数据路径流动。在实现上,我们用 RabbitMQ -> Redis -> WebSocket 实现一个流程。它是易失的。
冷日志则负责持久化和批量落盘,也就是真正的存储。 RabbitMQ -> MySQL。
异步快照存储:最终的快照是要将当前版本的 POI 刷盘存入 MinIO 中。
如何在不同的数据源中同步刷盘操作呢?在 Redis 与 MySQL 之间,我们用延时队列。也就是说,一旦有修改操作(不是自动保存操作,而是用户自动触发的),那么前端传递过来,这条数据就通过交换机入了 Redis 的队列成为消息;该消息的消费者会负责对 Redis 进行日志写入;如果写入成功则 ACK 进入串行的延时队列 MySQL,也就是与 MySQL 进行同步,我们设定一个延时范围,在一个不会过多降低性能也能够保证用户体验的程度上保证可靠性;2s 的消息丢失是可以忍受的;
在 MySQL 与 MinIO 中的刷盘同步也是如此;由于一旦日志进入 MySQL 持久化之后,那么这些内容可以被认为是一个完全可追溯的文档了。那么在任意时候,我们进行将该快照版本更新和重刷日志都是可以接受的;但是很明显,如果每次读取一个文件都要重刷大量日志的话,肯定是不必要的;也因此可以在一定条件出发后重刷新的快照版本,策略上可以是保证保留近期的3个历史快照;这个刷盘是异步的,可以通过延时队列或者计数器实现,也是一个非常简单的兜底策略。
这也会涉及到一个问题。如果刷盘更新快照开始了,那么意味着快照版本将要 +1, 那么原本没有更新的日志此时就会显示为老版本的快照。在我们当前的逻辑,一个文件是由当前快照内容+(日志快照号>=该快照)的内容组成的。那么