分页导出引发的数据隔离问题思考

简要业务上下文

  • 后台会定期导出单据核对,导出调用的是分页查询接口(按照创建时间倒序)
  • 运营又会在某时某刻新建一个单据
    p11

出现的问题

  • 当导出开始时,不管是串行查询,还是并行查询;期间如果有人新建一条数据,那么整个数据将有错乱的问题。如下图,Query1 执行完成后,同时插入一条新数据,那么之后的Query2将会又查询到id为831的数据,导致整个导出查询会有2条id为831的行。p12

思考

  • 解决当前这个问题,最快最高效的方法可能就是新增一个支持全量查询的接口服务,一次查询搞定;或者分页接口不按时间倒序,按照时间、ID升序
  • 抛开这个特定的业务场景,把问题抽象出来,可以发现这就像是数据库事务隔离的问题。如下图p13

Mysql 实践

启2个session,一个分批查询,一个在期间插入(当前表中没有记录)。

1. 查询不加事务,隔离机制的保护必然有一致性问题。
p14

2.加事务(使用默认隔离级别-‘REPEATABLE-READ’),可以发现插入被阻塞了,整个查询过程中,数据都没有被插入。此外,如果查询条件变成select count(id) from expense where id < 0,那么插入也不会阻塞,说明加锁是针对行级的。整个过程大致的机制是通过MVCC方式,对CRUD的操作做数据版本控制,比对版本,Redo等等。
p15

3.加事务,并把隔离级别改成‘READ-COMMITTED’,效果和1是一样的,因为其是不可重复读的。

MVCC

从数据库处理事务的机制,可以发现、借鉴这种MVCC思想,MVCC即Multiversion Concurrency Control。举个我们平时会用到的例子,AtomicInteger.compareAndSet(expect, update),Java的原子类CAS操作,其实就是把expect看成版本号;此外我们用的Tair分布式缓存,对某个Key肯定会有多并发,也是通过操作version来控制一致性。

对于像数据库,STM内部实现此机制,也同样会利用乐观、事务来处理并发控制,只不过数据结构、流程逻辑更加复杂。

回到最初的问题

在不新增全量接口,或者改变排序方式的情况下。可以把MVCC思想拿过来,业务场景中费用单记录只会新增、修改不会删除。那么在原接口不改的情况下,可以将查询结果的总量作为版本号来做比较(这里会有一个修改导致并发冲突,如期间某个小二修改某行,因为是基于导出之后修改之前的时间点,所以是可以容忍的),每次查询都与上一次查询返回的总量比对,如果不一致那么说明有数据插入,此时整个查询全部Redo。

当然了,业务场景不同,需求也不同。如果按上面的逻辑,导出如果一直遇到新增,那么也不知道等到猴年马月了。再往下优化的话(遐想),异步处理,dump缓存等等,其实都不如导出时拒绝新增请求,毕竟费用单导出核对,到这个点就冻结,拉出所有流水也很合理,有点像GC的“stop the world”,简单粗暴也挺好。

后来实践发现整个查询事务都加上一个时间戳去查询,比如创建时间小于XX,这个XX可以是最新的一条数据的时间戳,也可以第一次查询时的当前时间,总之这个时间戳每次查询不能变,也可保证查出来数据的一致性。

总结

兵无常势,水无常形,有些思想在某些问题上是触类旁通的,多发现,多总结,因地制宜,因需制效!

发表评论

电子邮件地址不会被公开。 必填项已用*标注

您可以使用这些HTML标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>