MongoDB的事务(2/3) 读操作 readPreference 和 readConcern

readConcern
  • 问题综述:(在读取数据的过程中需要关注以下两个问题)

      1. 从哪里读?即关注数据节点的位置。
      1. 什么样的数据可以读?关注数据的隔离性。
  • 第一个问题是由 readPreference 来解决的。

    • readPreference 在很早的版本就有了。

    • readPreference 以下五个可选值:

        1. primary:只选择主节点;
        1. primaryPreferred:优先选择主节点,如果不可用则选择从节点;
        1. secondary:只选择从节点;
        1. secondaryPreferred:优先选择从节点,如果从节点不可用则选择主节点;
        1. nearest:选择最近的节点(ping mongod服务器哪个最近就用哪个);
    • 场景举例:

      • 用户下订单后马上将用户转到订单详情页面——primary/primaryPreferred。因为此时从节点可能还没复制到新订单;
      • 用户查询自己下过的订单——secondary/secondaryPreferred。查询历史订单对实效性通常没有太高的要求;
      • 生成报表——secondary。报表对实效性要求不高,但资源需求大,可以在从节点单独处理,避免对线上用户造成影响;
      • 将用户上传的图片分发到全世界,让各地用户能够就近读取——nearest。每个地区的应用选择最近的节点读取数据;
    • readPreference 与 Tag

      • readPreference 只能控制使用一类节点。Tag则可以将节点选择控制到一个或几个节点。考虑以下场景:
        • 一个5节点的复制集;
        • 3个节点(1主2从)硬件较好,专用于服务线上用户(OLTP);
        • 2个节点(2从)硬件较差,专用于生成报表(OLAP);
      • 可以使用Tag来达到这样的控制目的:
      • 为3个较好的节点打上 {purpose:"online"};
      • 为2个较差的节点打上 {purpose:"analyse"};
      • 在线应用读取时指定online,报表读取时指定reporting。
    • readPreference 配置

      • 通过 MongoDB 的连接串参数:

        mongodb://host1:27017,host2:27017,host3:27017/?replicatSet=rs&readPreference=secondary
        
      • 通过 MongoDB驱动程序API:

        MongoCollection.withReadPreference(ReadPreference readPref)
      
      • Mongo Sehll(单个操作时指定):
      db.collection.find({...}).readPref("secondary")
      
    • readPreference 实验:从节点读

      • 主节点写入{x:1},观察该数据在各个节点立即可见。

        • 在两个从节点分别执行 db.fsyncLock() 来锁定写入(同步)。
        • 主节点写入
          • db.test.find({a:123})
          • db.test.find({a:123}).readPref("secondary")
        • 解除从节点锁定 db.fsyncUnlock()
          • db.test.find({a:123}).readPref("secondary")
      • 注意事项:

        • 指定 readPreference 时应注意高可用的问题。例如将 readPreference 指定 primary,则发生故障转移不存在 primary 期间将没有节点可读。如果业务允许,则应选择 primaryPreferred;
        • 使用 Tag 时也会遇到同样的问题,如果只有一个节点拥有一个特定 Tag,则在这个节点失效时将无节点可读。这在有时候时期望结果,有时候不是。例如:
          • 如果报表使用的节点失效,即使不生成报表,也不希望将报表负载转移到其他节点上,此时只有一个节点有报表 Tag 是合理的选择。
          • 如果线上节点失效,通常希望有替代节点,所以应该保持多个节点有同样的 Tag;
        • Tag 有时需要与优先级、选举权综合考虑。例如做报表的节点通常不会希望它作为主节点,则优先级应该为0。
  • 第二个问题是由 readConcern 来解决。

    • 什么是 readConcern
      • 在 readPreference 选择了指定的节点后,readConcern 决定这个节点上的数据哪些是可读的,类似于关系数据库的隔离级别。

      • 可选值包括:

        • available 读取所有可用的数据,什么都不管;(从节点读取时默认 avaliable)
        • local 读取所有可用且属于当前分片的数据;(主节点读取时默认 local)
          • local 和 avaliable 在没有分片集群的情况下是几乎一致的。
          • 但是在分片迁移(chunk)的过程中,可能会存在数据同时出现在两个分片集的情况,这时可能会出现数据重复的问题。
        • majority 读取在大多数节点上提交完成的数据;(推荐使用)
        • linearizable 可线性化读取文档,保证这次的读是可以读到上一次的写的;
        • snapshot 读取最近快照中的数据,差不多可以达到关系型数据库中序列化的级别,隔离级别最高的;
      • 注意事项:

        • 在主节点读取数据时默认 readConcern 是 local,在从节点读取数据时默认的 readConcern 是 avaliable的(出于向前兼容);
        • MongoDB <= 3.6 不支持对从节点使用 {readConcern: "local"};
        • 虽然看上去总应该选择 local,但会在对结果集过滤过程产生额外消耗。在一些无关紧要的场景(如统计)下,也可以考虑使用 avaliabel。
      • readConcern: "majority"

      • 实验:readConcern: "majority" vs "local"

        • 安装3节点复制集。

        • 注意在配置文件内 replication 的参数 enableMajorityReadConcern 需要打开。

          replication:
          	replSetName: rs0
          	enableMajorityReadConcern: true
        
        • 将复制集中两个从节点使用 db.fsyncLock() 锁住写入(模拟同步高延迟)。

          	db.test.save({"a":1}) # 主节点:写入。
          	db.fsyncLock() # 两个从节点:分别执行。
          	db.test.find().readConcern("local")
          	db.test.find().readConcern("majority") # 此时会阻塞
          	db.fsyncUnlock() # 在一个从节点上解除写锁。
          	db.test.find().readConcern("majority") # 这次可以成功读取。
          
        • 结论:

          • 使用 local 参数,则可以直接查询到写入数据。
          • 使用 majority,只能查询到已经被多数节点确认过的数据。
          • update 与 remove 与上同理。
      • readConcern: majority 与 脏读

        • MongoDB 中的回滚:
          • 写操作在到达大多数节点前都是不安全的,一旦主节点崩溃,从节点还没复制到此次操作,则刚才的写操作就丢失了。
          • 把一次写操作视为一个事务,从事务的角度,可以认为事务被回滚了。
        • 所以从分布式系统的角度来看,事务的提交被提升到了分布式集群多个节点级别的提交,而不再是单个节点上的提交。
        • 在可能发生回滚的前提下考虑脏读问题:
          • 如果在一次写操作到达大多数节点前读取了这个写操作,然后因为系统故障该操作回滚了,则发生了脏读问题;
          • 使用 {readConcern:"majority"} 可以有效避免脏读。
      • readConcern 如何实现安全的读写分离

        • 考虑如下场景:

          • 向主节点写入一条数据,立即在从节点读取这条数据。

          • 错误方式:

          db.orders.insert({old:101,sku:"kite",q:1})
          db.orders.find({old:101}).readPref("secondary")
          
          • 正确方式:writeConcern + readConcern majority 来解决:
          db.orders.insert({old:101,sku:"kite",q:1}, {writeConcern:{w:"majority"}})
          db.orders.find({old:101}).readPref("secondary").readConcern("majority")
          
        • 小测试

          • readConcern 主要关注读的隔离性,ACID 中的 Isolation,但是是分布式数据库特有的概念。
          • readConcern:"majority" 相应于事务中隔离级别中的 Read Committed。
      • readConcern:"linearizable" 略

      • readConcern:"snapshot"

        • 只在多文档事务中生效。
        • 讲一个事务的 readConcern 设置为 snapshot,将保证在事务中的读:
          • 不出现脏读;
          • 不出现不可重读;
          • 不出现幻读。
        • 因为所有的读都将使用同一个快照,直到事务提交为止该快照才被释放。

posted on 2021-03-31 16:51  Sweet小马  阅读(1283)  评论(0编辑  收藏  举报

导航