mongoDB的读写分离

一、读写分离相关的理论

  1.1、ReadPreference读偏好

  1.2脏数据

  1.3复制集的缺点

  1.4读隔离 Read Concern

  1.5写确认 Write Concern

二、springboot中实现读写分离

  2.1 MongoDB连接池指定读模式

  2.2、在代码层面动态切换

一、读写分离相关的理论

1.1、ReadPreference读偏好

在副本集Replica Set中才涉及到ReadPreference的设置,默认情况下,读写都是分发都Primary节点执行,但是对于写少读多的情况,我们希望进行读写分离来分摊压力,所以希望使用Secondary节点来进行读取,Primary只承担写的责任(实际上写只能分发到Primary节点,不可修改)。

MongoDB有5种ReadPreference模式:

  • primary: 主节点,默认模式,读操作只在主节点,如果主节点不可用,报错或者抛出异常。

  • primaryPreferred:首选主节点,大多情况下读操作在主节点,如果主节点不可用,如故障转移,读操作在从节点。

  • secondary:从节点,读操作只在从节点, 如果从节点不可用,报错或者抛出异常。

  • secondaryPreferred:首选从节点,大多情况下读操作在从节点,特殊情况(如单主节点架构)读操作在主节点。

  • nearest:最邻近节点,读操作在最邻近的成员,可能是主节点或者从节点。

1.2脏数据

其实说的就是 MongoDB 的数据持久化,在一个数据写到 journal 并 flush 到磁盘上之前,数据都是脏的,而在复制集内,数据会通过 Oplog 传播到其它节点上,然后重复写入的步骤。

假如这个过程中,主节点挂掉了,之前的某一个 Secondary 提升成为了 Primary,由于数据没有写到大部分节点上,于是新的 Primary 看不到之前的应该写入的新数据,即使这时候旧的 Primary 回来了,它也只能是 Secondary,它之前的那些新数据就会丢失,从而导致数据的回滚。

1.3复制集的缺点

说了优点之后,再说说它的缺点,毕竟 CAP 原理还是统治着分布式领域。在 CAP 原理中,C 表示一致性,A 表示一致性,P 表示分区容忍性。

MongoDB 的默认复制集配置是显然的 CP,因为 ReadPreference 默认为 Primary;如果换成 Secondary 或者 SecondaryPreferred,就相当于 AP 了,C 用了业界默认的最终一致性,因为它的复制是基于 Oplog 的异步方案。

但是,AP 方案容易导致的问题有复制延迟导致的:

注意:这些的例子只是随便举例,不一定会是真实情况。

  1. 写后读,或者说是读己写问题:即从 Primary 写入数据后,然后马上从 Secondary 读,这时候由于延迟问题而有可能在 Secondary 读不到最新数据,于是我刚发了个微博,刷新了下反而消失了,过一会儿又出现了;
  2. 单调读问题,或者说是时光倒流问题:这时候由于多次从不同的 Secondary 读取数据,比如微博的评论下面,如果两次读到的数据不一致后,容易导致先看到了回复,刷新后却消失了,再过一会儿又出现了;
  3. 因果读写不一致问题:与上面的微博例子相似,即出现在一个微博下面,评论的回复比评论先到达的现象;

解决的办法显然是有的,MongoDB 分别从读与写提供了解决方案,让你能够调整配置来取舍复制集中的 C 与 A。

1.4读隔离 Read Concern

目前一共有五种读隔离的设置:

  1. local:不保证数据都被写入了大部分节点,我们在使用的时候基本默认的选项;
  2. available:3.6 版本引入,与 因果一致性会话 有关,也是不保证数据都被写入了大部分节点,暂时还没用过;
  3. majority:保证数据都被写入了大部分节点,但是必须使用 WiredTiger 存储引擎;
  4. linearizable:这个也没有用过,意思也不是很清楚,文档大致意思理解为对文档所有的读写都是顺序,或者说线性执行的,会导致花费时间超过 majority,建议与 maxTimeMS 一起食用;
  5. snapshot:4.0 版本引入,与多文档的事务有关,也是没用过;

所以除了 local 与 majority,我都不能保证叙述的准确性,毕竟与实际用还是有区别的。但是基本上可以了解到:读隔离的效果是需要用时间去交换的,或者说降低可用性去交换的。

另外特别提一下这句文档中的话:

Regardless of the read concern level, the most recent data on a node may not reflect the most recent version of the data in the system.
不管 Read concern 的具体配置,节点上最新的数据,不一定意味着它也是系统中最新的数据。

因为不管 Read concern 如何配置,它始终是从单个节点读的,这个设计的初衷只能保证不读到脏数据。

1.5写确认 Write Concern

{ w: <value>, j: <boolean>, wtimeout: <number> }

对于 w 参数,则有三种,表示写入后得到多少个 Secondary 的确认后再返回:这三个参数,在进行写操作的时候非常有用,常见的设置便是将 j 设置为 true,表示等数据已经写入了磁盘上的 journal 后再返回,这时候即便数据库挂掉,也是能从 journal 中恢复的,注意这不是 oplog 它是高层次的日志,而 journal 是低层次的日志,是可以用来故障恢复后重建当前节点数据的日志5

  1. 数字:那就是确切的个数了;
  2. majority:自动帮你计算 n/2 + 1;
  3. tag set,标签组:即制定哪几个 tag 的 Secondary;

最后一个 wtimeout,则是在制定 w 参数的时候,推荐一并设置,防止超时,毕竟这种确认是牺牲性能的,很可能导致超时。

看到这里,大致可以得出结论,MongoDB 将读隔离与写确认交给客户端去取舍,一定程度上解决了复制延迟导致的业务问题,而本质上,这种解决方案的原理就在于用事务6

------------------------------------------------------------------------------------------------------

readConcern 的是为了在于解决脏读问题,用户从 MongoDB 的 primary 上读的数据并没有同步到大多数节点,然后 primary 宕机恢复, primary节点会将未同步到大多数节点的数据回滚,导致用户读到了脏数据。

当指定 readConcern 级别为majority ,能保证用户读到的数据已经写入到大多数节点,而这样的数据肯定不会发生回滚,避免了脏读的问题。

需要注意的是,readConcern 只是保证读到的数据不会发生回滚,但并不能保证读到的数据最新。

参考官网:

误区: majority并非从多节点读取,依然是单节点读取。

readConcern 原理

snapshot 0,1,2,3......N的状态是committed/uncommitted

同步到大多数节点时,对应的snapshot会标记为commmited。

用户读取:读最新的 commited 状态的 snapshot,这样就保证了读到的数据是已经同步到大多数节点。

secondary节点在自身oplog发生变化会同步信息到primary。

primary节点统计超过半数的节点的同步信息就修改该snapshot为uncommitted->commited。

同时secondary拉取oplog的同时从primary节点得到最新一条已经同步到大多数节点的oplog,更新自身的 snapshot 状态。

 ------------------------------------------------------------------------------------------------------

-----------------------------------------------------

mongodb 的读写一致性由 WriteConcern 和 ReadConcern 两个参数保证。

writeConcern

readConcern

两者组合可以得到不同的一致性等级。

指定 writeConcern:majority 可以保证写入数据不丢失,不会因选举新主节点而被回滚掉。

readConcern:majority + writeConcern:majority 可以保证强一致性的读

readConcern:local + writeConcern:majority 可以保证最终一致性的读

mongodb 对configServer全部指定writeConcern:majority 的写入方式,因此元数据可以保证不丢失。

对 configServer 的读指定了 ReadPreference:PrimaryOnly 的方式,在 CAP 中舍弃了A与P得到了元数据的强一致性读。

---------------------------------------------------

二、springboot中的MongoDB读写分离实现

2.1 MongoDB连接池指定读模式

再重申下在副本集Replica Set中才涉及到ReadPreference的设置才有意义。

连接池的配置中主要注意几个参数:

// 客户端配置(连接数、副本集群验证)
MongoClientOptions.Builder builder = new MongoClientOptions.Builder();
//...
builder.readPreference(ReadPreference.secondaryPreferred());
builder.readConcern(ReadConcern.MAJORITY);
//...
MongoClientOptions mongoClientOptions = builder.build();

 

xml示例(没有测试过):

<!-- mongodb配置 -->
<mongo:mongo id="mongo"  host="${mongo.host}" port="${mongo.port}" write-concern="NORMAL" >
    <mongo:options 
        connections-per-host="${mongo.connectionsPerHost}"
        threads-allowed-to-block-for-connection-multiplier="${mongo.threadsAllowedToBlockForConnectionMultiplier}"
        connect-timeout="${mongo.connectTimeout}" 
        max-wait-time="${mongo.maxWaitTime}"
        auto-connect-retry="${mongo.autoConnectRetry}" 
        socket-keep-alive="${mongo.socketKeepAlive}"
        socket-timeout="${mongo.socketTimeout}" 
        slave-ok="${mongo.slaveOk}"
        write-number="1" 
        write-timeout="0" 
        write-fsync="false"
    />
</mongo:mongo>

<!-- mongo的工厂,通过它来取得mongo实例,dbname为mongodb的数据库名,没有的话会自动创建 -->
<mongo:db-factory id="mongoDbFactory" dbname="uba" mongo-ref="mongo" />

<!-- 读写分离级别配置  -->
<!-- 首选主节点,大多情况下读操作在主节点,如果主节点不可用,如故障转移,读操作在从节点。 -->
<bean id="primaryPreferredReadPreference" class="com.mongodb.TaggableReadPreference.PrimaryPreferredReadPreference" />
<!-- 最邻近节点,读操作在最邻近的成员,可能是主节点或者从节点。  -->
<bean id="nearestReadPreference" class="com.mongodb.TaggableReadPreference.NearestReadPreference" />
<!-- 从节点,读操作只在从节点, 如果从节点不可用,报错或者抛出异常。存在的问题是secondary节点的数据会比primary节点数据旧。  -->
<bean id="secondaryReadPreference" class="com.mongodb.TaggableReadPreference.SecondaryReadPreference" />
<!-- 优先从secondary节点进行读取操作,secondary节点不可用时从主节点读取数据  -->
<bean id="secondaryPreferredReadPreference" class="com.mongodb.TaggableReadPreference.SecondaryPreferredReadPreference" />
<!-- mongodb的主要操作对象,所有对mongodb的增删改查的操作都是通过它完成 -->
<bean id="mongoTemplate" class="org.springframework.data.mongodb.core.MongoTemplate">
    <constructor-arg name="mongoDbFactory" ref="mongoDbFactory" />
    <property name="readPreference" ref="primaryPreferredReadPreference" />
</bean>

 

对应的配置(在建立mongoDB的连接时,指定ReadPreference)

请仔细看好 spring.data.mongodb.uri 的配置,他的格式如下,可以参考mongodb连接

mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[database][?options]]

例子:

# MongoDB URI配置 重要,添加了用户名和密码验证
spring.data.mongodb.uri=mongodb://zhuyu:zhuyu@192.168.68.138:27017,192.168.68.137:27017,192.168.68.139:27017/ai?slaveOk=true&replicaSet=zypcy&write=1&readPreference=secondaryPreferred&connectTimeoutMS=300000

#每个主机的连接数
spring.data.mongodb.connections-per-host=50
#线程队列数,它以上面connectionsPerHost值相乘的结果就是线程队列最大值
spring.data.mongodb.threads-allowed-to-block-for-connection-multiplier=50
spring.data.mongodb.connect-timeout=5000
spring.data.mongodb.socket-timeout=3000
spring.data.mongodb.max-wait-time=1500
#控制是否在一个连接时,系统会自动重试
spring.data.mongodb.auto-connect-retry=true
spring.data.mongodb.socket-keep-alive=true

验证读写分离是否生效:

创建一个 Rest风格的 IndexController ,提供:添加与查询接口,访问这2个接口,看控制台输出,是否查操作自动分配到从库,写操作分配到主库

@RequestMapping("/index")
@RestController
public class IndexController {

    @Autowired private MongoTemplate mongoTemplate;

    @RequestMapping("/getList")
    public List<TestModel> getList(){
        List<TestModel> list = mongoTemplate.findAll(TestModel.class,"test");
        return list;
    }

    @RequestMapping("/add")
    public String add(){
        TestModel model = new TestModel("zhuyu" + System.currentTimeMillis());
        mongoTemplate.insert(model , "test");
        return "success";
    }
}

2.2、在代码层面动态切换

通过mongoTemplate对象动态指定 mongoTemplate.setReadPreference(readPreference);

例如,在同一个应用中定义2个mongoTemplate对象,一个设置从primary读,一个设置从Secondary读,根据应用场景选择不同的mongoTemplate

 

三、MongoDB读写分离验证

 

调整优先级的方法1:

改优先级,登录指定shard主节点,mongo ip:22001 -u root --password=xxxx --authenticationDatabase admin
1. 先删除节点,rs.remove("ip1:22002")
2. 再添加回节点,指定优先级
rs.add({
_id: 0,
host: "ip1:22002",
priority: 5
})
3. 执行rs.reconfig()使配置生效
rs.add({
_id: 0,
host: "ip1:22002",
priority: 5
})

调整优先级的方法2:

 

 分别进行读/写的场景压测,看服务器资源的消耗情况就知道读写分离是否生效了。

转自:

https://blog.csdn.net/zhuyu19911016520/article/details/82998162?depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-3&utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-3

https://blog.xizhibei.me/2019/05/05/mongodb-replica-set/

https://blog.csdn.net/cxu123321/article/details/108897067

 

posted on 2020-04-14 11:57  duanxz  阅读(8898)  评论(0编辑  收藏  举报