最近的一个业务系统,需要实现一套操作日志的记录方式。这篇日志算是对各种实现的一个思考。
一 每个操作记录流水
这种方式非常适合每一步操作都非常重要的系统。比如常见的金融操作系统。在一般的场景下,我们只会对涉及金钱的操作记录这种流水日志,比如转账,扣款等。
在设计账户流水表的时候,需要有以下几个必须的字段:
(1) instruction_id 流水号
(2) amount 金额
(3) operation_type 操作类型(转账or扣款)
(4) before_balance 操作前账户余额
(5) after_balance 操作后账户余额
(6) time 操作时间
第一个字段,流水号,是主键。这个字段的必要性在于,它能维护该记录修改的幂等性。简单的实现,流水号可以使用uuid。
当前端发起一个充值的操作时,首先在前端产生一个流水号,然后将其他的用户填写的参数通过rpc请求发送到后端。后端的处理流程如下:
(1) 开启事务
(2) 从db中查询账户余额
(3) 修改账户余额
(4) 记录流水
(5) 提交事务
假设在执行过程中,出现了网络问题,该执行结果处于未知状态,那么前端可以通过刚才的uuid反复重试,直到得到一个确定的结果为止。
我们可以对流水表稍做改进,以适应普遍的情况。字段如下:
(1) instruction_id 流水号(在对可靠性要求不太高的场景下,可使用数据库自增id)
(2) operation_type 操作类型
(3) before (一个json,描述修改前的该行记录)
(4) after (json, 描述修改后的该行记录)
(5) time
还有一些可选的字段,比如 input_param(输入参数),操作人等等。
这种基于流水的方式优点有:
1 可靠性很高
2 实现方式确定,可以基于接口做注解实现
3 可以基于该流水,实现业务层的操作回滚
但是它的缺点也很明显:
1 增加开发量,需要额外增加记录写入
2 降低运行效率,需要多一到两次查询(before, after)
3 和业务绑定的比较紧
二 基于数据库的binlog实现
第二种方式是基于mysql的binlog来实现。这种方式最大的好处是可以和业务完全的解耦,而且统计的结果是完全准确的(相比于有些完全基于业务代码的实现来说,这类实现一般是对执行前后查询两次)。
关于binlog的定义及获取,可以参考
binlog可以描述为下面这种model:
@Data
public class RowDiffModel {
long timestamp;
String tableName;
List<String> pkColumnName = new ArrayList<>(); //主键列
List<Object> pk = new ArrayList<>();
int type; //1 新建 //2 更新 //3 删除
List<String> diffColumns = new ArrayList<>();
Map<String, Object> preValue = new HashMap<>();
Map<String, Object> newValue = new HashMap<>();
}
基于binlog的实现还有一个小问题。如何获得用户的操作id?在一些涉及权限授权及权限分离的系统中,操作id非常重要。但由于这个字段完全隶属于业务层,和数据库的设计关联度并不大。
为了解决这个问题,我们需要在每个涉及用户操作的表中增加一个新的字段operator_id。
还有一种情况是,用户的某一步操作涉及多个表的修改,这个时候可以按事务id将binlog聚集起来。
之后,我们需要将binlog转换为一条操作记录,记录到库里,操作记录的字段如下:
(1) operator_id (操作人id)
(2) operation_type (操作类型,可根据表及binlog类型(insert,update,delete)确定)
(3) before (一个json,描述本行记录修改前的值)
(4) after (描述本行记录修改后的值)
(5) timestamp (时间戳)
现在,用户操作日志的生成及查询,整体流程如下:
1 用户操作修改表
2 开启异步服务获取binlog
3 根据事务id将binlog做一定的聚合处理
4 将binlog转换为一条原始的操作日志,并记录到库里
5 当用户查询操作日志时,根据条件检索操作日志
6 将操作日志转换为一个用户可读的view返回
网友评论