读写分离和弱读在事务中相关问题咨询

【 使用环境 】生产环境
【 OB or 其他组件 】oceanbase
【 使用版本 】4.2.1.8
【问题描述】
集群为1-1-1方案。
初次从mysql迁移到oceanbase。
为了防止由于跨机关联带来的性能问题。
全部表都加入了一个大表组。目前所有表的主副本都在zone1上。
下图为集群信息:

为了用上从副本的机器性能,obproxy,设置了优先读从副本,参数如下

事务数据一致性测试:
执行以下sql。
– 开启事务
start TRANSACTION;

– 读表
select /** testte */ id
,s_sku
from prod_s_info where id = 1;
– 修改表
update prod_s_info set remark = ‘test’ where id = 1;

– 再次读表
select /** testte2 */ id
,s_sku
from prod_s_info where id = 1;

– 提交事务
commit;

– 查看读sql执行路由
select * from GV$OB_SQL_AUDIT
where QUERY_SQL like ‘%testte%’
and QUERY_SQL not like ‘select * from GV$OB_SQL_AUDIT%’

发现2次读,都是路由到了从副本进行读。开启事务无法保证数据一致性。

设置的优先读从副本了,强读要设置读主副本

这个需要每个sql都 设置吗。
还是有ob参数能让 开启了事务的会话都读 主副本

https://www.oceanbase.com/docs/common-oceanbase-database-cn-1000000001053058


可以先学习下这篇文章

4 个赞
  1. 你的实验里查询 gv$ob_sql_audit返回的字段太少了,所以不能看出什么问题。还需要返回:sid, tx_id , plan_type
  2. odp 4.2.2 版本以前的事务路由特点是当dml语句开启事务后,在事务提交之前所有sql的路由都跟dml 的第一条sql路由相同。你担心的那个有道理,但是把所有表都放到一个表分组的用法恰恰是表分组被误用最厉害的用法。当所有表都在一个表分组里,表分组就没有意义了。你完全可以设置租户的 primary_zone='zone1’就实现所有的分区的主副本都在一个zone里(这里也是一个节点)。
  3. odp 4.2.2以及更高版本后,事务里的sql 可以直接路由到sql对应的分区主副本,不怕有分布式事务。这是新功能,还需要观察。

所有表放到一个表组里面,只是我们第一期的迁移做法。
后续还是要做分区。将分区主副本打散到所有机器上的。

但是我看了下,如果设置了primary_zone=‘zone1’. 后续要迁移表的主副本到其他zone。没有什么好的解决方案。

所以就通过表组来控制, 先将所有表放到一个表组里面,来避免跨机查询的问题。
后面我们通过业务调整后,再逐渐将表从这个表组里面移除来。并通过分区将主副本打散到3个zone上。

1 个赞

恩,用意我理解了。

还需要个关键信息,这个事务里的表的实际主副本位置。你发下下面这个 SQL 结果(换成你的库/schema名和表名)

select database_name, table_id,table_name, partition_name, tablet_id,ls_id ,zone,svr_ip, tablegroup_name 
from DBA_OB_TABLE_LOCATIONS
where database_name in ('TPCC') and role in ('LEADER') and table_name in ('BMSQL_ITEM','BMSQL_WAREHOUSE');

如果是 mysql 租户,DBA_OB_TABLE_LOCATIONS 就是 oceanbase.DBA_OB_TABLE_LOCATIONS ;如果是 oracle 租户,DBA_OB_TABLE_LOCATIONS 就是 sys.DBA_OB_TABLE_LOCATIONS

2 个赞

感谢解惑,我也遇到类似问题了

之前忽略了一个问题,prod_s_info表是个复制表。测试可能不正确。
换了一个表。测试结果也是一致的。
主副本在ip尾数129这台服务器。


1 个赞

emm。还是发生了一致性问题。
事务内数据是能保证一致性。
但是连续的事务,无法保证一致性。
例如2个连续的事务都对A表做的修改。
事务1: 查询A表中 字段f1 = 0的改为1.
事务2: 查询A表中 字段f1 = 0的改为2.
读写分离,无法保证事务2一定从主副本读,从副本可能存在延迟。就导致数据实际被改成了2

目前生产上已经出现了这种数据不一致导致的问题了···
准备从应用层上解决这个问题。
未开启事务,使用读写分离的obproxy连接。
开启事务,使用强读主副本的obproxy连接

1 个赞

不太理解你描述的场景
未提交会产生锁的

1 个赞

for(int i = 0; i < 1000 ;i++){
– 开启事务
start transaction;
– java查询id为10的数据
select id,f1 from tableA where id = 10;
– 将字段f1自增1
java执行 f1++;
– 更新id = 10的数据
update tableA set f1 = #{f1} where id = 10;
– 提交事务
commit
}

执行过程中,由于查询的是从副本数据,不能保证事务一开始读到的是最新数据。
最终f1的结果 < 1000

测试代码如下:


表结构
image
循环1000遍。
最终f1 = 410.
image

因此读写分离模式下, 即使开始事务,最终也无法保证数据一致性。

计划从应用层面上解决这个问题;
使用 强读主副本 和读写分离两种代理。
当开启事务时,使用强读主副本的代理

你这是错误的用法。OB 事务的读一致性取决于 事务中的第一条语句

如果第一条是 INSERT/UPDATE/DELETE,或者 SELECT FOR UPDATE,那本次事务就是强一致性的。
如果第一条语句是 SELECT,就取决于该语句的 弱一致性读 设置。因为你全局设置了 弱一致性读,那么第一条语句也就是 弱一致性读 的。

此外,即使OB保证了事务内的强一致性,你这种用法仍然是错的。
比如某些资金交易类型的操作,先开启事务执行了某些数据更新操作,在事务提交之后,又需要 SELECT 查询某些数据。如果查询的数据对前面更新的数据有业务依赖,即使没有开启事务,也是不应该走弱一致性读的。

比较合理的读写分离设计目标是这样的:

  1. 如果开启了事务,就要确保强一致性。
  2. 如果当前线程之前已经开启过事务,那么该线程(及其 异步子线程)接下来的所有数据库查询,都应该优先保证强一致性(除非明确可以弱一致性)【线程复用时,记得 ThreadLocal 状态重置】。
  3. 有些对一致性要求较高的场景(比如 交易前的用户资金检查),即使没开启过事务,这次查询也应该优先保证强一致性。
  4. 对一些数据一致性要求不高的统计查询(这些也最容易产生慢查询,耗费资源),就可以强制开启弱一致性读(哪怕该线程之前已经执行过事务)。

我们建议的实现思路是:
不要全局开启弱一致性读。而是在程序中定义两个数据源,再自己实现 Spring 提供的动态数据源接口org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource,检测到 @Transactional 注解 或 之前已经开启事务,或 满足其他自定义条件 就访问 主库 数据源,否则访问 从库 数据源。

从库 可以配置一个只读账号,然后在 JDBC URL 参数里面加上 &sessionVariables=ob_read_consistency=WEAK 即可,亲测可用。

3 个赞

1.

13 楼的回答已经指出一些问题,我再补充一下这段代码在高并发下的问题。
单纯看这段代码是很典型的场景,事务里先读后更新。 先不提开启了全局弱一致性读那个事情。这段代码即使放在oracle 数据库上也有个潜在的问题,就是并发跑的时候。
先以oracle 为例说(撇开ob的弱一致性读因素干扰)。事务开始先从表 tableA读出f1,然后 f1 用于业务计算再更新会表 tableA. 这个读出f1 在oracle 里是快照读,不会被其他事务的锁阻塞,也不会阻塞其他事务。(这里默认事务隔离级别是 read committed)。所以并发跑这段代码的时候,很可能会出现 f1 被交叉覆盖。 所以如果业务上是要先读出这个数据处理后再更新回去,一般的做法是读 到时候加锁,用 select … for update 。这个也叫“丢失更新“。
在ob里,oracle和mysql租户默认的事务隔离级别都是 read-committed,读是mvcc读,也不会阻塞写或被写阻塞,所以这段代码同样有风险。读也建议用 select … for update 。

当在ob里开启了全局弱一致性读时,由于这个select 是在 update 之前,select 可以独立路由。还用了弱一致性读,同样也不会被update 阻塞,也不会阻塞update。有同样的问题。这里也确实如上面所说不适合读写分离。
只要将select 改为 select for update,业务逻辑正确了,也不会出现读写分离了。

2.

如果事务中update 后面还有一次那个 select 才提交事务,那么这个 select 在这里依然是弱一致性读。这个在业务上也是有问题的。在事务里,一般要求的是写后要求立即能读,即强一致性读。
proxysql 的读写分离方案里事务里的读不会路由到从节点。

3.

odp 4.2.2 参数 enable_transaction_internal_routing 默认是 true,事务中 sql都独立路由。只要将 租户 primary_zone设置为 random,适当结合表分组控制一些表的主副本集中一起 ,全部的数据主副本还是分散在多个节点,那么业务读写基本也是分散,也能实现多个节点同时提供读写服务,或者说实现了完全的分布式。那么在这种情形下,“读写分离”必要性就不是 那么大了。

读写分离使用场景一般是那些复杂查询比较集中的报表业务,专门为它配置一个 开启了 弱一致性读的 ODP ,并且专门给一个备副本或者只读副本用于集中式读。对于主要交易业务使用的ODP还是默认配置(不开弱一致性读)。

1 个赞

是的。这里只是模拟历史代码的思路。
解决方案和我提到的使用2种代理思路是一样的。 开启事务时,数据源使用强读主代理连接。
其他默认使用从库优先

这里只是模拟一下,历史代码中,循环执行事务的场景。
这种场景基本就是一条线程在跑。 不会存在并发。
由于代码编写使用的是mysql主从架构的思维,基本前提就是开启事务必定读主库。
由于刚开始从mysql迁移到oceanbase。
这种情况引发的数据不一致问题比较明显,所以提出来了。

有并发的情况的更新,都是 select for update。 或者并发的数据行不相同。
这种反而暂时还未发现数据不一致的情况

1 个赞

除非你们的慢查询特别多,或者查询消耗特别多的资源,那么其实并不建议使用 单一主副本 + 读写分离。
我们之前也实现了读写分离,但后来我们又取消掉了。

主要考虑到以下几点:

  1. OB 自带请求超时机制,超过 10s(默认阈值,可调整) 的查询就会被中止,不会超长时间的消耗资源(部分慢查询建议通过 SQL Hint 的方式自定义超时时间)。
  2. OB 有慢查询抑制机制,当CPU资源不足,快查询和慢查询产生争用时,慢查询将让出调度优先权,可将慢查询的CPU占用限制在最多30%(默认阈值,可调整)以内。这在一定程度上能够抑制 慢查询 的资源占用,避免 正常的快查询 也被“株连九族”。
  3. 查 从库,会有 数据一致性 的延迟问题,有可能读到脏数据(数据库CPU、磁盘IO 负载高时,还是比较可能出现),除非你的业务能够容忍。
  4. 如果你有 30 个数据表,使用 OB 最常见的3副本集群,OB 大概率会将 每10表 主副本 放在 不同的 Zone。比如【表1~10】 的主副本在Zone 1,【表11~20】 的主副本在Zone 2,【表21~30】 的主副本在Zone 3。默认是 强一致性读,对应表的数据读写也都访问的是其 主副本 所在的 Zone。这样本身也是相对比较均衡的。它并不是像 MySQL 那样——所有数据表的 写入都是在一个主节点。有些大数据表可以采用分区表,OB会按照表分区数量进行负载均衡(比如创建16个分区,每个Zone会各分配5个左右)。
  5. 至于跨Zone的处理开销,这个实测其实影响不大,而且我们还可以创建表组,将需要关联查询、需要在一个事务中进行处理的多个数据表放入同一个表组中。
1 个赞

是个老业务库了。所有模块都在一个库里面,从mysql迁移到oceanbase。总共1400+表。

一开始做的迁移规划,也是按照最标准的将主副本打散到3个zone上。
对于关联查询的表,都做了分区且加同一表组的处理。
还有些关联比较广泛的表也是改为了复制表。
但由于历史sql代码比较多。
第一次切换上线的时候,发现cpu还是被少量的跨机查询sql拖累。 经常飙升到90%以上。
所以又改为了现在的切换方案。
第一步: primary zone的方案还是random。但是我们建了个大表组,所有表都不分区,且都加到这个大表组里面。这样就暂时不用考虑跨机查询的问题。
而由于原mysql是一主一从架构才能hold住日常业务请求。所以就搭建了2个obproxy:一个 读写分离 + 最大容忍延迟400ms; 一个强读主副本。

后续我们还会做第二期的表分区改造,一个个业务模块改造为分区表,并将表从这个大表组里面移除来。将主副本打散到3个zone里面。这样最终就能实现只需要一个obproxy: 读主副本。

你可以拆分成几个小的表组,某些表在业务上需要联表查询或进行事务处理,就放在一个表组。
不过目前 OB 的负载均衡 做得还不是太好,它是按照表的数量来进行负载均衡的。
比如你有60个表(或表分区),它默认给3个Zone的每个Zone各随机分配20个。

但按照表分区数量进行负载均衡不是一个好主意。因为某些表数据量特别大、读写特别多,使用的资源负载就会很多;有些表只是存几条配置数据,基本上没什么开销。
按照每个表的 数据量、CPU/IO 占用 来进行负载均衡才更好,OB 后续应该会支持让我们给每个表单独设置权重,不过目前还不支持。
所以,你先创建好多个表组,然后等到它负载均衡之后,你看看每个Zone的主副本分布,如果负载不均衡可能就需要再将表组微调一下。

1 个赞

老师,您那边可以复现一下帮忙确认查询语句的sql_audit里的CONSISTENCY_LEVEL字段,如果确认是弱读,那就符合预期读到了一个旧版本数据。如果非弱读提供一下多次查询截图和完整步骤。包括完整的事务的observer.log日志