基于 MyBatis 手撸一个分表插件

da2a1b91e5207b4efe3486e49967802a.png

背景

事情是酱紫的,上级leader负责记录信息的业务,每日预估数据量是15万左右,所以引入sharding-jdbc做分表。

上级leader完成业务的开发后,走了一波自测,git push后,就忙其他的事情去了。

项目的框架是SpringBoot+Mybaits

出问题了

因为负责的业务也开发完了,熟练的git pull,准备自测,单元测试run一下,上个厕所回来收工,就是这么自信。

0433307bda1e6240926256ae25ff6a83.gif

回来后,看下控制台,人都傻了,一片红,内心不禁感叹“如果这是股票基金该多好”。

出了问题就要解决,随着排查深入,我的眉头一皱发现事情并不简单,怎么以前的一些代码都报错了?

58f3a74d5b3aca59abc41ec4bb7fa9c8.png

随着排查深入,最后跟到了Mybatis源码,发现罪魁祸首是sharding-jdbc引起的,因为数据源是sharding-jdbc的,导致后续执行sql的是ShardingPreparedStatement

这就意味着,sharding-jdbc影响项目的所有业务表,因为最终数据库交互都由ShardingPreparedStatement去做了,历史的一些sql语句因为sql函数或者其他写法,使得ShardingPreparedStatement无法处理而出现异常。

关键代码如下

8e5b534a7fe6fea9cd48df1643cb5404.png

发现问题后,阿星马上就反馈给leader了。

55900a4a4fc918d930ef599e8977f045.png

唉,本来还想摸鱼的,看来摸鱼的时间是没了,还多了一项任务。

分析

竟然交给阿星来做了,就撸起袖子开干吧,先看看分表功能的需求

  • 支持自定义分表策略

  • 能控制影响范围

  • 通用性

分表会提前建立好,所以不需要考虑表不存在的问题,核心逻辑实现,通过分表策略得到分表名,再把分表名动态替换到sql

8371056152f26433e75bf7f5d5e67cc8.png

分表策略

为了支持分表策略,我们需要先定义分表策略抽象接口,定义如下

/*** @Author 程序猿阿星* @Description 分表策略接口* @Date 2021/5/9*/
public interface ITableShardStrategy {/*** @author: 程序猿阿星* @description: 生成分表名* @param tableNamePrefix 表前缀名* @param value 值* @date: 2021/5/9* @return: java.lang.String*/String generateTableName(String tableNamePrefix,Object value);/*** 验证tableNamePrefix*/default void verificationTableNamePrefix(String tableNamePrefix){if (StrUtil.isBlank(tableNamePrefix)) {throw new RuntimeException("tableNamePrefix is null");}}
}

generateTableName函数的任务就是生成分表名,入参有tableNamePrefix、valuetableNamePrefix为分表前缀,value作为生成分表名的逻辑参数。

verificationTableNamePrefix函数验证tableNamePrefix必填,提供给实现类使用。

为了方便理解,下面是id取模策略代码,取模两张表

/*** @Author 程序猿阿星* @Description 分表策略id* @Date 2021/5/9*/
@Component
public class TableShardStrategyId implements ITableShardStrategy {@Overridepublic String generateTableName(String tableNamePrefix, Object value) {verificationTableNamePrefix(tableNamePrefix);if (value == null || StrUtil.isBlank(value.toString())) {throw new RuntimeException("value is null");}long id = Long.parseLong(value.toString());//此处可以缓存优化return tableNamePrefix + "_" + (id % 2);}
}

传入进来的valueid值,用tableNamePrefix拼接id取模后的值,得到分表名返回。

控制影响范围

分表策略已经抽象出来,下面要考虑控制影响范围,我们都知道Mybatis规范中每个Mapper类对应一张业务主体表,Mapper类的函数对应业务主体表的相关sql

阿星想着,可以给Mapper类打上注解,代表该Mpaaer类对应的业务主体表有分表需求,从规范来说Mapper类的每个函数对应的主体表都是正确的,但是有些同学可能不会按规范来写。

假设Mpaaer类对应的是B表,Mpaaer类的某个函数写着A表的sql,甚至是历史遗留问题,所以注解不仅仅可以打在Mapper类上,同时还可以打在Mapper类的任意一个函数上,并且保证小粒度覆盖粗粒度。

阿星这里自定义分表注解,代码如下

/*** @Author 程序猿阿星* @Description 分表注解* @Date 2021/5/9*/
@Target(value = {ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface TableShard {// 表前缀名String tableNamePrefix();//值String value() default "";//是否是字段名,如果是需要解析请求参数改字段名的值(默认否)boolean fieldFlag() default false;// 对应的分表策略类Class<? extends ITableShardStrategy> shardStrategy();}

注解的作用范围是类、接口、函数,运行时生效。

tableNamePrefixshardStrategy属性都好理解,表前缀名和分表策略,剩下的valuefieldFlag要怎么理解,分表策略分两类,第一类依赖表中某个字段值,第二类则不依赖。

根据企业id取模,属于第一类,此处的value设置企业id入参字段名,fieldFlagtrue,意味着,会去解析获取企业id字段名对应的值。

根据日期分表,属于第二类,直接在分表策略实现类里面写就行了,不依赖表字段值,valuefieldFlag无需填写,当然你value也可以设置时间格式,具体看分表策略实现类的逻辑。

通用性

抽象分表策略与分表注解都搞定了,最后一步就是根据分表注解信息,去执行分表策略得到分表名,再把分表名动态替换到sql中,同时具有通用性。

Mybatis框架中,有拦截器机制做扩展,我们只需要拦截StatementHandler#prepare函数,即StatementHandle创建Statement之前,先把sql里面的表名动态替换成分表名。

Mybatis分表拦截器流程图如下

0c0c133da3eb7c792ff9abcdf0c20f72.png

Mybatis分表拦截器代码如下,有点长哈,主流程看intercept函数就好了。

/*** @Author 程序员阿星* @Description 分表拦截器* @Date 2021/5/9*/
@Intercepts({@Signature(type = StatementHandler.class,method = "prepare",args = {Connection.class, Integer.class})
})
public class TableShardInterceptor implements Interceptor {private static final ReflectorFactory defaultReflectorFactory = new DefaultReflectorFactory();@Overridepublic Object intercept(Invocation invocation) throws Throwable {// MetaObject是mybatis里面提供的一个工具类,类似反射的效果MetaObject metaObject = getMetaObject(invocation);BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");MappedStatement mappedStatement = (MappedStatement)metaObject.getValue("delegate.mappedStatement");//获取Mapper执行方法Method method = invocation.getMethod();//获取分表注解TableShard tableShard = getTableShard(method,mappedStatement);// 如果method与class都没有TableShard注解或执行方法不存在,执行下一个插件逻辑if (tableShard == null) {return invocation.proceed();}//获取值String value = tableShard.value();//value是否字段名,如果是,需要解析请求参数字段名的值boolean fieldFlag = tableShard.fieldFlag();if (fieldFlag) {//获取请求参数Object parameterObject = boundSql.getParameterObject();if (parameterObject instanceof MapperMethod.ParamMap) { //ParamMap类型逻辑处理MapperMethod.ParamMap parameterMap = (MapperMethod.ParamMap) parameterObject;//根据字段名获取参数值Object valueObject = parameterMap.get(value);if (valueObject == null) {throw new RuntimeException(String.format("入参字段%s无匹配", value));}//替换sqlreplaceSql(tableShard, valueObject, metaObject, boundSql);} else { //单参数逻辑//如果是基础类型抛出异常if (isBaseType(parameterObject)) {throw new RuntimeException("单参数非法,请使用@Param注解");}if (parameterObject instanceof Map){Map<String,Object>  parameterMap =  (Map<String,Object>)parameterObject;Object valueObject = parameterMap.get(value);//替换sqlreplaceSql(tableShard, valueObject, metaObject, boundSql);} else {//非基础类型对象Class<?> parameterObjectClass = parameterObject.getClass();Field declaredField = parameterObjectClass.getDeclaredField(value);declaredField.setAccessible(true);Object valueObject = declaredField.get(parameterObject);//替换sqlreplaceSql(tableShard, valueObject, metaObject, boundSql);}}} else {//无需处理parameterField//替换sqlreplaceSql(tableShard, value, metaObject, boundSql);}//执行下一个插件逻辑return invocation.proceed();}@Overridepublic Object plugin(Object target) {// 当目标类是StatementHandler类型时,才包装目标类,否者直接返回目标本身, 减少目标被代理的次数if (target instanceof StatementHandler) {return Plugin.wrap(target, this);} else {return target;}}/*** @param object* @methodName: isBaseType* @author: 程序员阿星* @description: 基本数据类型验证,true是,false否* @date: 2021/5/9* @return: boolean*/private boolean isBaseType(Object object) {if (object.getClass().isPrimitive()|| object instanceof String|| object instanceof Integer|| object instanceof Double|| object instanceof Float|| object instanceof Long|| object instanceof Boolean|| object instanceof Byte|| object instanceof Short) {return true;} else {return false;}}/*** @param tableShard 分表注解* @param value      值* @param metaObject mybatis反射对象* @param boundSql   sql信息对象* @author: 程序猿阿星* @description: 替换sql* @date: 2021/5/9* @return: void*/private void replaceSql(TableShard tableShard, Object value, MetaObject metaObject, BoundSql boundSql) {String tableNamePrefix = tableShard.tableNamePrefix();//获取策略classClass<? extends ITableShardStrategy> strategyClazz = tableShard.shardStrategy();//从spring ioc容器获取策略类ITableShardStrategy tableShardStrategy = SpringUtil.getBean(strategyClazz);//生成分表名String shardTableName = tableShardStrategy.generateTableName(tableNamePrefix, value);// 获取sqlString sql = boundSql.getSql();// 完成表名替换metaObject.setValue("delegate.boundSql.sql", sql.replaceAll(tableNamePrefix, shardTableName));}/*** @param invocation* @author: 程序猿阿星* @description: 获取MetaObject对象-mybatis里面提供的一个工具类,类似反射的效果* @date: 2021/5/9* @return: org.apache.ibatis.reflection.MetaObject*/private MetaObject getMetaObject(Invocation invocation) {StatementHandler statementHandler = (StatementHandler) invocation.getTarget();// MetaObject是mybatis里面提供的一个工具类,类似反射的效果MetaObject metaObject = MetaObject.forObject(statementHandler,SystemMetaObject.DEFAULT_OBJECT_FACTORY,SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY,defaultReflectorFactory);return metaObject;}/*** @author: 程序猿阿星* @description: 获取分表注解* @param method* @param mappedStatement* @date: 2021/5/9* @return: com.xing.shard.interceptor.TableShard*/private TableShard getTableShard(Method method, MappedStatement mappedStatement) throws ClassNotFoundException {String id = mappedStatement.getId();//获取Classfinal String className = id.substring(0, id.lastIndexOf("."));//分表注解TableShard tableShard = null;//获取Mapper执行方法的TableShard注解tableShard = method.getAnnotation(TableShard.class);//如果方法没有设置注解,从Mapper接口上面获取TableShard注解if (tableShard == null) {// 获取TableShard注解tableShard = Class.forName(className).getAnnotation(TableShard.class);}return tableShard;}}

到了这里,其实分表功能就已经完成了,我们只需要把分表策略抽象接口、分表注解、分表拦截器抽成一个通用jar包,需要使用的项目引入这个jar,然后注册分表拦截器,自己根据业务需求实现分表策略,在给对应的Mpaaer加上分表注解就好了。

c5462d0899f528abdbcf7389dabe7da4.png

实践跑起来

这里阿星单独写了一套demo,场景是有两个分表策略,表也提前建立好了

  • 根据id分表

    • tb_log_id_0

    • tb_log_id_1

  • 根据日期分表

    • tb_log_date_202105

    • tb_log_date_202106

预警:后面都是代码实操环节,请各位读者大大耐心看完(非Java开发除外)

TableShardStrategy定义

/*** @Author wx* @Description 分表策略日期* @Date 2021/5/9*/
@Component
public class TableShardStrategyDate implements ITableShardStrategy {private static final String DATE_PATTERN = "yyyyMM";@Overridepublic String generateTableName(String tableNamePrefix, Object value) {verificationTableNamePrefix(tableNamePrefix);if (value == null || StrUtil.isBlank(value.toString())) {return tableNamePrefix + "_" +DateUtil.format(new Date(), DATE_PATTERN);} else {return tableNamePrefix + "_" +DateUtil.format(new Date(), value.toString());}}
}*** @Author 程序猿阿星* @Description 分表策略id* @Date 2021/5/9*/
@Component
public class TableShardStrategyId implements ITableShardStrategy {@Overridepublic String generateTableName(String tableNamePrefix, Object value) {verificationTableNamePrefix(tableNamePrefix);if (value == null || StrUtil.isBlank(value.toString())) {throw new RuntimeException("value is null");}long id = Long.parseLong(value.toString());//可以加入本地缓存优化return tableNamePrefix + "_" + (id % 2);}
}

Mapper定义

Mapper接口

/*** @Author 程序猿阿星* @Description* @Date 2021/5/8*/
@TableShard(tableNamePrefix = "tb_log_date",shardStrategy = TableShardStrategyDate.class)
public interface LogDateMapper {/*** 查询列表-根据日期分表*/List<LogDate> queryList();/*** 单插入-根据日期分表*/void  save(LogDate logDate);}-------------------------------------------------------------------------------------------------/*** @Author 程序猿阿星* @Description* @Date 2021/5/8*/
@TableShard(tableNamePrefix = "tb_log_id",value = "id",fieldFlag = true,shardStrategy = TableShardStrategyId.class)
public interface LogIdMapper {/*** 根据id查询-根据id分片*/LogId queryOne(@Param("id") long id);/*** 单插入-根据id分片*/void save(LogId logId);}

Mapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xing.shard.mapper.LogDateMapper">//对应LogDateMapper#queryList函数<select id="queryList" resultType="com.xing.shard.entity.LogDate">selectid as id,comment as comment,create_date as createDatefromtb_log_date</select>//对应LogDateMapper#save函数<insert id="save" >insert into tb_log_date(id, comment,create_date)values (#{id}, #{comment},#{createDate})</insert>
</mapper>-------------------------------------------------------------------------------------------------<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xing.shard.mapper.LogIdMapper">//对应LogIdMapper#queryOne函数<select id="queryOne" resultType="com.xing.shard.entity.LogId">selectid as id,comment as comment,create_date as createDatefromtb_log_idwhereid = #{id}</select>//对应save函数<insert id="save" >insert into tb_log_id(id, comment,create_date)values (#{id}, #{comment},#{createDate})</insert></mapper>

执行下单元测试

日期分表单元测试执行

@Testvoid test() {LogDate logDate = new LogDate();logDate.setId(snowflake.nextId());logDate.setComment("测试内容");logDate.setCreateDate(new Date());//插入logDateMapper.save(logDate);//查询List<LogDate> logDates = logDateMapper.queryList();System.out.println(JSONUtil.toJsonPrettyStr(logDates));}

输出结果

dd632a09e31e4148214c3f394c1d7877.png

id分表单元测试执行

@Testvoid test() {LogId logId = new LogId();long id = snowflake.nextId();logId.setId(id);logId.setComment("测试");logId.setCreateDate(new Date());//插入logIdMapper.save(logId);//查询LogId logIdObject = logIdMapper.queryOne(id);System.out.println(JSONUtil.toJsonPrettyStr(logIdObject));}

输出结果

09ec4af70b8d331038697d8ba1c5b457.png

小结一下

本文可以当做对Mybatis进阶的使用教程,通过Mybatis拦截器实现分表的功能,满足基本的业务需求,虽然比较简陋,但是Mybatis这种扩展机制与设计值得学习思考。

有兴趣的读者也可以自己写一个,或基于阿星的做改造,毕竟是简陋版本,还是有很多场景没有考虑到。

另外分表的demo项目,放到了Gitee和公众号,大家按需自取

- Gitee地址: https://gitee.com/jxncwx/shard

项目结构:

04a4b0041b5cd3927bae34f97562f2b6.png

855bbf93cc97d113b28e82b4c8148e58.gif

往期推荐

81c9e419c7c7bedeb6567f5fdd03171c.png

MyBatis 中为什么不建议使用 where 1=1?


8c3cf029078bcc946ec4548da883bc78.png

SpringBoot 使用注解实现消息广播功能


185bf9ff5cd5ebec2d64782e6ab45bd6.png

聊聊接口性能优化的11个小技巧


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/544465.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

密码学哈希函数_哈希函数在密码学中的应用

密码学哈希函数A Hash Function is a mathematical function that converts a numerical value into another compressed numeric value. The input value for the hash functions can be of arbitrary length, but the output text that it will produce will always be of fi…

C语言图形化界面——含图形、按钮、鼠标、进度条等部件制作(带详细代码、讲解及注释)

目录0.引言1.素材准备2.编程2.1.创建你的界面2.2.创建按钮2.3.鼠标操作2.3.1.单击特效2.3.2.光标感应2.3.3.进度条3.完整代码及效果0.引言 \qquad看了CSDN上很多关于C程序图形化界面的介绍&#xff0c;有的代码繁琐难解&#xff0c;不方便调试修改&#xff1b;有的不够详细。本…

【MATLAB】无人驾驶车辆的模型预测控制技术(精简讲解和代码)【运动学轨迹规划】

文章目录<font color#19C>0.友情链接<font color#19C>1.引言<font color#19C>2.预测模型<font color#19C>3.滚动优化<font color#08CF>3.1.线性化3.2.UrU_rUr​的求取<font color#08CF>3.3.离散化与序列化<font color#08CF>3.4.实现…

顶级Javaer,常用的 14 个类库

作者&#xff1a;小姐姐味道&#xff08;微信公众号ID&#xff1a;xjjdog&#xff09;昨天下载下来Java16尝尝鲜。一看&#xff0c;好家伙&#xff0c;足足有176MB大。即使把jmc和jvisualvm给搞了出去&#xff0c;依然还是这么大&#xff0c;真的是让人震惊不已。但即使JDK足够…

单层神经网络线性回归_单层神经网络| 使用Python的线性代数

单层神经网络线性回归A neural network is a powerful tool often utilized in Machine Learning because neural networks are fundamentally very mathematical. We will use our basics of Linear Algebra and NumPy to understand the foundation of Machine Learning usin…

面试官:说一下 final 和 final 的 4 种用法?

作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;重要说明&#xff1a;本篇为博主《面试题精选-基础篇》系列中的一篇&#xff0c;查看系列面试文章请关注我。Gitee 开源地址…

面试官:int和Integer有什么区别?为什么要有包装类?

作者 | 磊哥来源 | Java面试真题解析&#xff08;ID&#xff1a;aimianshi666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;重要说明&#xff1a;本篇为博主《面试题精选-基础篇》系列中的一篇&#xff0c;查看系列面试文章请关注我。Gitee 开…

innodb是如何存数据的?yyds

前言如果你使用过mysql数据库&#xff0c;对它的存储引擎&#xff1a;innodb&#xff0c;一定不会感到陌生。众所周知&#xff0c;在mysql5以前&#xff0c;默认的存储引擎是&#xff1a;myslam。但mysql5之后&#xff0c;默认的存储引擎已经变成了&#xff1a;innodb&#xff…

【MATLAB】卡尔曼滤波器的原理及仿真(初学者专用)

文章目录0.引言1.场景预设2.卡尔曼滤波器3.仿真及效果0.引言 \qquad本文参考了Matlab对卡尔曼滤波器的官方教程及帮助文档&#xff08;Kalman Filter&#xff09;。官方教程的B站链接如下&#xff0c;在此对分享资源的Up主表示感谢。(如不能正常播放或需要看中文字幕&#xff0…

Go实现查找目录下(包括子目录)替换文件内容

为什么80%的码农都做不了架构师&#xff1f;>>> 【功能】 按指定的目录查找出文件&#xff0c;如果有子目录&#xff0c;子目录也将进行搜索&#xff0c;将其中的文件内容进行替换。 【缺陷】 1. 没有过滤出文本文件 2. 当文件过大时&#xff0c;效率不高 【代码】…

卡诺模板_无关条件的卡诺地图

卡诺模板Till now, the Boolean expressions which have been discussed by us were completely specified, i.e., for each combination of input variable we have specified a minterm by representing them as 1 in the K-Map. But, there may arise a case when for a giv…

面试官:final、finally、finalize 有什么区别?

作者 | 磊哥来源 | Java面试真题解析&#xff08;ID&#xff1a;aimianshi666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;重要说明&#xff1a;本篇为博主《面试题精选-基础篇》系列中的一篇&#xff0c;查看系列面试文章请关注我。Gitee 开…

【Matlab】扩展卡尔曼滤波器原理及仿真(初学者入门专用)

文章目录0.引言及友情链接1.场景预设2.扩展卡尔曼滤波器3.仿真及效果0.引言及友情链接 \qquad卡尔曼滤波器&#xff08;Kalman Filter, KF&#xff09;是传感器融合&#xff08;Sensor Fusion&#xff09;的基础&#xff0c;虽然知乎、CSDN、GitHub等平台已有大量的学习资料&am…

Windows 8.1 升级到专业版

本例将一台 Windows 8.1 平板升级到专业版。升级前&#xff1a;升级的原因&#xff0c;是因为用户发现这台平板不能启用远程桌面管理。查看计算机属性&#xff0c;显示如下&#xff1a;从上面的信息可以看出&#xff0c;目前这台平板安装的不是专业版。具体是什么版本呢&#x…

【MATLAB】求点到多边形的最短距离

文章目录0.引言1.原理2.代码及实用教程0.引言 \qquad点与多边形的关系无非三种——内部、上、外部。本文定义点在多边形内部距离为负&#xff0c;点在多边形边上距离为0&#xff0c;到多边形外部距离为正。 1.原理 计算点到多边形的距离分为3个步骤&#xff1a; 判断点与多边…

【Python】mmSegmentation语义分割框架教程(自定义数据集、训练设定、数据增强)

文章目录0.mmSegmentation介绍1.mmSegmentation基本框架1.1.mmSegmentation的model设置1.2.mmSegmentation的dataset设置1.2.1.Dataset Class文件配置1.2.2.Dataset Config文件配置1.2.3.Total Config文件配置2.运行代码 3.展示效果图和预测X.附录X.1.mmSegmentation框架解释 X…

面试官:重写 equals 时为什么一定要重写 hashCode?

作者 | 磊哥来源 | Java面试真题解析&#xff08;ID&#xff1a;aimianshi666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;重要说明&#xff1a;本篇为博主《面试题精选-基础篇》系列中的一篇&#xff0c;关注我&#xff0c;查看更多面试题。…

【python】获取PC机公网IP并发送至邮箱

文章目录0.引言1.获取外网IP2.打开SMTP服务3.python发送邮件4.完整代码0.引言 \qquad之前一直使用Putty连接公司的PC机进行远程办公&#xff0c;苦于外网的IP地址不能固定下来&#xff0c;所以购买了内网穿透服务&#xff0c;免费版还会限速。后来转念一想&#xff0c;如果能定…

List 去重的 6 种方法,这个方法最完美!

作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;在日常的业务开发中&#xff0c;偶尔会遇到需要将 List 集合中的重复数据去除掉的场景。这个时候可能有同学会问&#xff1a…

Mongodb -(3) replica set+sharding

分片集搭建---何旭东目录分片集搭建...................................................................................................................... 1生态系统...............................................................................................…