分库分表的事务处理机制

转载自 分库分表的事务处理机制

分布式事务

 

由于我们将单表的数据切片后存储在多个数据库甚至多个数据库实例中,所以依靠数据库本身的事务机制不能满足所有场景的需要。但是,我们推荐在一个数据库实例中的操作尽可能使用本地事务来保证一致性,跨数据库实例的一系列更新操作需要根据事务路由在不同的数据源中完成,各个数据源之间的更新操作需要通过分布式事务处理。

 

这里只介绍实现分布式操作一致性的几个主流思路,保证分布式事务一致性的具体方法请参考《分布式服务架构:原理、设计与实战》中第2章的内容。

主流的分布式事务解决方案有三种:两阶段提交协议、最大努力保证模式和事务补偿机制。

 

1两阶段提交协议

 

两阶段提交协议将分布式事务分为两个阶段,一个是准备阶段,一个是提交阶段,两个阶段都由事务管理器发起。基于两阶段提交协议,事务管理器能够最大限度地保证跨数据库操作的事务的原子性,是分布式系统环境下最严格的事务实现方法。符合J2EE规范的AppServer(例如:Websphere、Weblogic、

Jboss等)对关系型数据库数据源和消息队列都实现了两阶段提交协议,只需在使用时配置即可。如图3-9所示。

 

 

 

但是,两阶段提交协议也带来了性能方面的问题,难于进行水平伸缩,因为在提交事务的过程中,事务管理器需要和每个参与者进行准备和提交的操作的协调,在准备阶段锁定资源,在提交阶段消费资源,但是由于参与者较多,锁定资源和消费资源之间的时间差被拉长,导致响应速度较慢,在此期间产生死锁或者不确定结果的可能性较大。因此,在互联网行业里,为了追求性能的提升,很少使用两阶段提交协议。

 

另外,由于两阶段提交协议是阻塞协议,在极端情况下不能快速响应请求方,因此有人提出了三阶段提交协议,解决了两阶段提交协议的阻塞问题,但仍然需要事务管理器在参与者之间协调,才能完成一个分布式事务。

 

 

2最大努力保证模式

 

这是一种非常通用的保证分布式一致性的模式,很多开发人员一直在使用,但是并未意识到这是一种模式。最大努力保证模式适用于对一致性要求并不十分严格但是对性能要求较高的场景。

 

具体的实现方法是,在更新多个资源时,将多个资源的提交尽量延后到最后一刻处理,这样的话,如果业务流程出现问题,则所有的资源更新都可以回滚,事务仍然保持一致。唯一可能出现问题的情况是在提交多个资源时发生了系统问题,比如网络问题等,但是这种情况是非常罕见的,一旦出现这种情况,就需要进行实时补偿,将已提交的事务进行回滚,这和我们常说的TCC模式有些类似。

 

下面是使用最大努力保证模式的一个样例,在该样例中涉及两个操作,一个是从消息队列消费消息,一个是更新数据库,需要保证分布式的一致性。

  • 开始消息事务。

  • 开始数据库事务。

  • 接收消息。

  • 更新数据库。

  • 提交数据库事务。

  • 提交消息事务。

这时,从第1步到第4步并不是很关键,关键的是第5步和第6步,需要将其放在最后一起提交,尽最大努力保证前面的业务处理的一致性。到了第5步和第6步,业务逻辑处理完成,这时只可能发生系统错误,如果第5步失败,则可以将消息队列和数据库事务全部回滚,保持一致。如果第5步成功,第6步遇到了网络超时等问题,则这是唯一可能产生问题的情况,在这种情况下,消息的消费过程并没有被提交到消息队列,消息队列可能会重新发送消息给其他消息处理服务,这会导致消息被重复消费,但是可以通过幂等处理来保证消除重复消息带来的影响。

 

当然,在使用这种模式时,我们要充分考虑每个资源的提交顺序。我们在生产实践中遇到的一种反模式,就是在数据库事务中嵌套远程调用,而且远程调用是耗时任务,导致数据库事务被拉长,最后拖垮数据库。因此,上面的案例涉及的是消息事务嵌套数据库事务,在这里必须进行充分评估和设计,才可以规避事务风险。

 

3事务补偿机制

 

显然,在对性能要求很高的场景中,两阶段提交协议并不是一种好方案,最大努力保证模式也会使多个分布式操作互相嵌套,有可能互相影响。这里,我们给出事务补偿机制,其性能很高,并且能够尽最大可能地保证事务的最终一致性。

 

在数据库分库分表后,如果涉及的多个更新操作在某一个数据库范围内完成,则可以使用数据库内的本地事务保证一致性;对于跨库的多个操作,可通过补偿和重试,使其在一定的时间窗口内完成操作,这样就可以实现事务的最终一致性,突破事务遇到问题就滚回的传统思路。

 

如果采用事务补偿机制,则在遇到问题时,我们需要记录遇到问题的环境、信息、步骤、状态等,后续通过重试机制使其达到最终一致性,详细内容可以参考《分布式服务架构:原理、设计与实战》第2章,彻底理解ACID原理、CAP理论、BASE原理、最终一致性模式等内容。

 

事务路由

 

无论使用上面哪种方法实现分布式事务,都需要对分库分表的多个数据源路由事务,一般通过对Spring环境的配置,为不同的数据源配置不同的事务管理器(TransactionManager),这样,如果更新操作在一个数据库实例内发生,便可以使用数据源的事务来处理。对于跨数据源的事务,可通过在应用层使用最大努力保证模式和事务补偿机制来达成事务的一致性。当然,有时我们需要通过编写程序来选择数据库的事务管理器,根据实现方式的不同,可将事务路由具体分为以下三种。

 

1自动提交事务路由


自动提交事务通过依赖JDBC数据源的自动提交事务特性,对任何数据库进行更新操作后会自动提交事务,不需要开发人员手工操作事务,也不需要配置事务,实现起来很简单,但是只能满足简单的业务逻辑需求。

 

在通常情况下,JDBC在连接创建后默认设置自动提交为true,当然,也可以在获取连接后手工修改这个属性,代码如下:

 

connnection conn = null;  
try{  conn = getConnnection();  conn.setAutoCommit(true);  // 数据库操作……………………………conn.commit();  
}catch(Throwable e){  if(conn!=null){  try {  conn.rollback();  } catch (SQLException e1) {  e1.printStackTrace();  }  }  throw new RuntimeException(e);  
}finally{  if(conn!=null){  try {  conn.close();  } catch (SQLException e) {  e.printStackTrace();  }  }  
}

 

我们基本不需要使用原始的JDBC API来改变这些属性,这些操作一般都会被封装在我们使用的框架中。3.6节介绍的开源数据库分库分表框架dbsplit默认使用的就是这种模式。

 

2可编程事务路由

 

我们在应用中通常采用Spring的声明式的事务来管理数据库事务,在分库分表时,事务处理是个问题,在一个需要开启事务的方法中,需要动态地确定开启哪个数据库实例的事务,也就是说在每个开启事务的方法调用前就必须确定开启哪个数据源的事务。下面使用伪代码来说明如何实现一个可编程事务路由的小框架。

 

首先,通过Spring配置文件展示可编程事务小框架是怎么使用的:

<?xml version="1.0?>
<beans><bean id="sharding-db-trx0"class="org.springframework.jdbc.datasource.Data SourceTransactionManager"><property name="dataSource"><ref bean="sharding-db0" /></property></bean><bean id="sharding-db-trx1"class="org.springframework.jdbc.datasource.DataSourceTransactionMana ger"><property name="dataSource"><ref bean="sharding-db1" /></property></bean><bean id="sharding-db-trx2"class="org.springframework.jdbc.datasource.DataSourceTransactionMana ger"><property name="dataSource"><ref bean="sharding-db2" /></property></bean><bean id="sharding-db-trx3"class="org.springframework.jdbc.datasource.DataSourceTransactionMana ger"><property name="dataSource"><ref bean="sharding-db3" /></property></bean><bean id="shardingTransactionManager" class="com.robert.dbsplit.core. ShardingTransactionManager"><property name="proxyTransactionManagers"><map value-type="org.springframework.transaction.PlatformTran sactionManager"><entry key="sharding0" value-ref="sharding-db-trx0" /><entry key="sharding1" value-ref="sharding-db-trx1" /><entry key="sharding2" value-ref="sharding-db-trx2" /><entry key="sharding3" value-ref="sharding-db-trx3" /></map></property></bean><aop:config><aop:advisor advice-ref="txAdvice" pointcut="execution(* com.robert.biz.*insert(..))"/><aop:advisor advice-ref="txAdvice" pointcut="execution(* com.robert.biz.*update(..))"/><aop:advisor advice-ref="txAdvice" pointcut="execution(* com.robert.biz.*delete(..))"/></aop:config><tx:advice id="txAdvice" transaction-manager="shardingTransactionManager"><tx:attributes><tx:method name="*" rollback-for="java.lang.Exception"/></tx:attributes></tx:advice></beans>

 

这里使用Spring环境的aop和tx标签来拦截com.robert.biz包下的所有插入、更新和删除的方法,当指定的包的方法被调用时,就会使用Spring提供的事务Advice,Spring的事务Advice(tx:advice)会使用事务管理器来控制事务,如果某个方法发生了异常,那么Spring的事务Advice就会使shardingTransactionManager回滚相应的事务。

 

我们看到shardingTransactionManager的类型是ShardingTransactionManager,这个类型是我们开发的一个组合的事务管理器,这个事务管理器聚合了所有分片数据库的事务管理器对象,然后根据某个标记来路由到不同的事务管理器中,这些事务管理器用来控制各个分片的数据源的事务。

 

这里的标记是什么呢?我们在调用方法时,会提前把分片的标记放进ThreadLocal中,然后在ShardingTransactionManager的getTransaction方法被调用时,取得ThreadLocal中存的标记,最后根据标记来判断使用哪个分片数据库的事务管理器对象。

 

为了通过标记路由到不同的事务管理器,我们设计了一个专门的ShardingContextHolder类,在该类的内部使用了一个ThreadLocal类来指定分片数据库的关键字,在ShardingTransaction Manager中通过取得这个标记来选择具体的分片数据库的事务管理器对象。因此,这个类提供了setShard和getShard的方法,setShard用于使用者编程指定使用哪个分片数据库的事务管理器,而getShard用于ShardingTransactionManager获取标记并取得分片数据库的事务管理器对象。相关代码如下:

 

public class ShardingContextHolder<T> {private static final ThreadLocal shardHolder = new ThreadLocal();public static <T> void setShard(T shard) {Validate.notNull(shard, "请指定某个分片数据库!");shardHolder.set(shard);}public static <T> T getShard() {return (T) shardHolder.get();}
}

 

有了ShardingContextHolder类后,我们就可以在ShardingTransactionManager中根据给定的分片配置将事务操控权路由到不同分片的数据库的事务管理器上,实现很简单,如果在ThreadLocal中存储了某个分片数据库的事务管理器的关键字,就使用那个分片的数据库的事务管理器:

 

public class ShardingTransactionManager implements PlatformTransactionManager {private Map<Object, PlatformTransactionManager> proxyTransactionManagers =new HashMap<Object, PlatformTransactionManager>();protected PlatformTransactionManager getTargetTransactionManager() {Object shard = ShardingContextHolder.getShard();Validate.notNull(shard, "必须指定一个路由的shard!");return targetTransactionManagers.get(shard);}public void setProxyTransactionManagers(Map<Object, PlatformTransaction Manager> targetTransactionManagers) {this.targetTransactionManagers = targetTransactionManagers;}public void commit(TransactionStatus status) throws TransactionException {getProxyTransactionManager().commit(status);}public TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException {return getProxyTransactionManager().getTransaction(definition);}public void rollback(TransactionStatus status) throws TransactionException {getProxyTransactionManager().rollback(status);}
}

 

有了这些使用类,我们的可编程事务路由小框架就实现了,这样在某个具体的服务开始之前,我们就可以使用如下代码来控制使用某个分片的数据库的事务管理器了:

RoutingContextHolder.setShard("sharding0");
return userService.create(user);

 

3声明式事务路由

在上一小节实现了可编程事务路由的小框架,这个小框架通过让开发人员在ThreadLocal中指定数据库分片并编程实现。大多数分库分表框架会实现声明式事务路由,也就是在实现的服务方法上直接声明事务的处理注解,注解包含使用哪个数据库分片的事务管理器的信息,这样,开发人员就可以专注于业务逻辑的实现,把事务处理交给框架来实现。

 

下面是笔者在实际的线上项目中实现的声明式事务路由的一个使用实例:

 

@TransactionHint(table = "INVOICE", keyPath = "0.accountId")public void persistInvoice(Invoice invoice) {// Save invoice to DBthis.createInvoice(invoice);for (InvoiceItem invoiceItem : invoice.getItems()) {invoiceItem.setInvId(invoice.getId());invoiceItemService.createInvoiceItem(invoice.getAccountId(), invoiceItem);}// Save invoice to cacheinvoiceCacheService.set(invoice.getAccountId(), invoice.getInvPeriodStart().getTime(), invoice.getInvPeriodEnd().getTime(),invoice);// Update last invoice date to AccountAccount account = new Account();account.setId(invoice.getAccountId());account.setLstInvDate(invoice.getInvPeriodEnd());accountService.updateAccount(account);}

 

在这个实例中,我们开发了一个持久发票的服务方法。持久发票的服务方法用来保存发票信息和发票项的详情信息,这里,发票与发票项这两个领域对象具有父子结构关系。由于在设计过程中通过账户ID对这个父子表进行分库分表,因此在进行事务路由时,也需要通过账户ID控制使用哪个数据库分片的事务管理器。在这个实例中,我们配置了 TransactionHint,TransactionHint的声明如下:

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TransactionHint {String table() default "";String keyPath() default "";
}

可以看到,TransactionHint包含了两个属性,第1个属性table指定这次操作涉及分片的数据库表,第2个属性指定这次操作根据哪个参数的哪个字段进行分片路由。该实例通过table指定了INVOICE表,并通过keyPath指定了使用第1个参数的字段accountId作为路由的关键字。

 

这里的实现与可编程事务路由的小框架实现类似,在方法persistInvoice被调用时,根据TransactionHint提供的操作的数据库表名称,在Spring环境的配置中找到这个表的分库分表的配置信息,例如:一共分了多少个数据库实例、数据库和表。

 

下面是在Spring环境中配置的INVOICE表和INVOICE_ITEM表的具体信息,我们看到它们一共使用了两个数据库实例,每个实例有两个库,每个库有8个表,使用水平下标策略。配置如下:

 

<bean name="billingInvSplitTable" class="com.robert.dbsplit.core.Split Table"init-method="init"><property name="dbNamePrefix" value="billing_inv"/><property name="tableNamePrefix" value="INVOICE"/><property name="dbNum" value="2"/><property name="tableNum" value="8"/><property name="splitStrategyType" value="HORIZONTAL"/><property name="splitNodes"><list><ref bean="splitNode0"/><ref bean="splitNode1"/></list></property><property name="readWriteSeparate" value="true"/></bean><bean name="billingInvItemSplitTable" class="com.robert.dbsplit.core.SplitTable"init-method="init"><property name="dbNamePrefix" value="billing_inv"/><property name="tableNamePrefix" value="INVOICE_ITEM"/><property name="dbNum" value="2"/><property name="tableNum" value="8"/><property name="splitStrategyType" value="HORIZONTAL"/><property name="splitNodes"><list><ref bean="splitNode0"/><ref bean="splitNode1"/></list></property><property name="readWriteSeparate" value="true"/></bean>

然后,在方法被调用时通过AOP进行拦截,根据TransactionHint配置的路由的主键信息keyPath ="0.accountId",得知这次根据第0个参数Invoice的accountID字段来路由,根据Invoice的accountID的值来计算这次持久发票表具体涉及哪个数据库分片,然后把这个数据库分片的信息保存到ThreadLocal中。具体的实现代码如下:

 

SimpleSplitJdbcTemplate simpleSplitJdbcTemplate =(SimpleSplitJdbcTemplate) ReflectionUtil.getFieldValue(field SimpleSplitJdbcTemplate, invocation.getThis());Method method = invocation.getMethod();
// Convert to th method of implementation class
method = targetClass.getMethod(method.getName(), method.getParameter Types());TransactionHint[] transactionHints = method.getAnnotationsByType (TransactionHint.class);
if (transactionHints == null || transactionHints.length < 1)throw new IllegalArgumentException("The method " + method + " includes illegal transaction hint.");
TransactionHint transactionHint = transactionHints[0];String tableName = transactionHint.table();
String keyPath = transactionHint.keyPath();String[] parts = keyPath.split("\\.");
int paramIndex = Integer.valueOf(parts[0]);Object[] params = invocation.getArguments();
Object splitKey = params[paramIndex];if (parts.length > 1) {String[] paths = Arrays.copyOfRange(parts, 1, parts.length);splitKey = ReflectionUtil.getFieldValueByPath(splitKey, paths);
}SplitNode splitNode = simpleSplitJdbcTemplate.decideSplitNode(tableName, splitKey);ThreadContextHolder.INST.setContext(splitNode);

ThreadContextHolder是一个单例的对象,在该对象里封装了一个ThreadLocal,用来存储某个方法在某个线程下关联的分片信息:

 

public class ThreadContextHolder<T> {public static final ThreadContextHolder<SplitNode> INST = new ThreadContextHolder<SplitNode>();private ThreadLocal<T> contextHolder = new ThreadLocal<T>();public T getContext() {return contextHolder.get();}public void setContext(T context) {contextHolder.set(context);}
}

 

接下来与可编程式事务路由类似,实现一个定制化的事务管理器,在获取目标事务管理器时,通过我们在ThreadLocal中保存的数据库分片信息,获得这个分片数据库的事务管理器,然后返回:

 

public class RoutingTransactionManager implements PlatformTransactionManager {protected PlatformTransactionManager getTargetTransactionManager() {SplitNode splitNode = ThreadContextHolder.INST.getContext();return splitNode.getPlatformTransactionManager();}public void commit(TransactionStatus status) throws TransactionException {getTargetTransactionManager().commit(status);}public TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException {return getTargetTransactionManager().getTransaction(definition);}public void rollback(TransactionStatus status) throws TransactionException {getTargetTransactionManager().rollback(status);}
}

 

本节介绍的开源数据库分库分表框架dbsplit是一个分库分表的简单示例实现,在笔者所工作的公司内部有内部版本,在内部版本中实现了声明式事务路由,但是这部分功能并没有开源到dbsplit项目,原因是有些与业务结合的逻辑无法分离。如果感兴趣,则可以加入我们的开源项目开发中。

 

 

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

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

相关文章

如何优雅的使用RabbitMQ

RabbitMQ无疑是目前最流行的消息队列之一&#xff0c;对各种语言环境的支持也很丰富&#xff0c;作为一个.NET developer有必要学习和了解这一工具。消息队列的使用场景大概有3种&#xff1a; 1、系统集成&#xff0c;分布式系统的设计。各种子系统通过消息来对接&#xff0c;这…

hutool中身份证工具-IdcardUtil

JAVA工具例大全--根据身份编号获取户籍省份 发布于 2020-10-10 | 后端技术 | 浏览&#xff08;61&#xff09; | 评论&#xff08;0&#xff09;开场语(刷新后不一样):人生若只如初见&#xff0c;何事秋风悲画扇。作为一名IT人&#xff0c;你当然也想有自己一片天地&…

[Asp.Net Core轻量级Aop解决方案]AspectCore Project 介绍

AspectCore Project 介绍 什么是AspectCore Project ? AspectCore Project 是适用于Asp.Net Core 平台的轻量级 Aop(Aspect-oriented programming) 解决方案&#xff0c;它更好的遵循Asp.Net Core的模块化开发理念&#xff0c;使用AspectCore可以更容易构建低耦合、易扩展的We…

放松眼球的网站

http://www.spielzeugz.de/html5/liquid-particles-3D/

异步广度优先搜索算法

为什么要异步&#xff1f; CPU的工艺越来越小&#xff0c;Cannon Lake架构的Intel CPU已经达到10nm技术&#xff0c;因此在面积不变的情况下&#xff0c;核心数可以明显提升。单纯的提升主频将造成发热量大、需要的电压大、功耗大的问题。而传统的算法与数据结构是针对单核心单…

开箱即用 - jwt 无状态分布式授权

基于JWT(Json Web Token)的授权方式 JWT 是JSON风格轻量级的授权和身份认证规范&#xff0c;可实现无状态、分布式的Web应用授权&#xff1b; 从客户端请求服务器获取token&#xff0c; 用该token 去访问实现了jwt认证的web服务器。 token 可保存自定义信息&#xff0c;如用户基…

Java类加载器总结

转载自 Java类加载器总结 1.类的加载过程 JVM将类加载过程分为三个步骤&#xff1a;装载&#xff08;Load&#xff09;&#xff0c;链接&#xff08;Link&#xff09;和初始化(Initialize)链接又分为三个步骤&#xff0c;如下图所示&#xff1a; 1) 装载&#xff1a;查找并…

MyBatis-Plus EntityWrapper的使用 wrapper le ge

https://blog.csdn.net/shujuelin/article/details/99568651 MyBatis-Plus EntityWrapper的使用 脚丫先生 2019-08-14 14:43:43 2660 收藏 分类专栏&#xff1a; javaee 版权 调度Airflow 本专刊主要以调度系统Airflow详细讲解(会把工作中对于调度系统的docker容器化部署、…

又踩.NET Core的坑:在同步方法中调用异步方法Wait时发生死锁(deadlock)

之前在将 Memcached 客户端 EnyimMemcached 迁移 .NET Core 时被这个“坑”坑的刻骨铭心&#xff08;详见以下链接&#xff09;&#xff0c;当时以为只是在构造函数中调用异步方法&#xff08;注&#xff1a;这里的异步方法都是指基于Task的&#xff09;才会出线死锁&#xff0…

jvm类加载器以及双亲委派

转载自 jvm类加载器以及双亲委派 首先来了解几个概念&#xff1a; 类加载&#xff1a; 概念&#xff1a;虚拟机把描述类的数据从Class文件加载到内存&#xff0c;并对数据进行校验--转换解析--初始化&#xff0c;最终形成能被java虚拟机直接使用的java类型&#xff0c;就是jvm…

分布式系统搭建:服务发现揭秘

CAP理论 加州大学终身教授与著名计算机科学家Eric Allen Brewer在90年代末提出了CAP理论&#xff0c;理论断言任一个基于网络的分布式系统&#xff0c;最多只能满足“数据一致性”、“可用性”、“分区容错性”三要素中的两个要素。 该理论后被MIT证明可行&#xff0c;故架构师…

SSL / TLS 协议运行机制详解

转载自 SSL / TLS 协议运行机制详解 互联网的通信安全&#xff0c;建立在SSL/TLS协议之上。 本文简要介绍SSL/TLS协议的运行机制。文章的重点是设计思想和运行过程&#xff0c;不涉及具体的实现细节。如果想了解这方面的内容&#xff0c;请参阅RFC文档。 一、作用 不使用SS…

在ASP.NET Core Web API上使用Swagger提供API文档

我在开发自己的博客系统&#xff08;http://daxnet.me&#xff09;时&#xff0c;给自己的RESTful服务增加了基于Swagger的API文档功能。当设置IISExpress的默认启动路由到Swagger的API文档页面后&#xff0c;在IISExpress启动Web API站点后&#xff0c;会自动重定向到API文档页…

一文告诉你 Java RMI 和 RPC 的区别

转载自 一文告诉你 Java RMI 和 RPC 的区别 RPC 远程过程调用 RPC&#xff08;Remote Procedure Call Protocol&#xff09;远程过程调用协议&#xff0c;通过网络从远程计算机上请求调用某种服务。一次RPC调用的过程大概有10步&#xff1a; 1.执行客户端调用语句&#xff…

Java架构师必须知道的 6 大设计原则

转载自 Java架构师必须知道的 6 大设计原则 在软件开发中&#xff0c;前人对软件系统的设计和开发总结了一些原则和模式&#xff0c; 不管用什么语言做开发&#xff0c;都将对我们系统设计和开发提供指导意义。本文主要将总结这些常见的原则&#xff0c;和具体阐述意义。 开发…

Flux --gt; Redux --gt; Redux React 入门 基础实例教程

本文的目的很简单&#xff0c;介绍Redux相关概念用法 及其在React项目中的基本使用 假设你会一些ES6、会一些React、有看过Redux相关的文章&#xff0c;这篇入门小文应该能帮助你理一下相关的知识 一般来说&#xff0c;推荐使用 ES6ReactWebpack 的开发模式&#xff0c;但Webpa…

mybatisplus 强制制空 空覆盖原来的字符串

ApiModelProperty(value "证件照片url") TableField(value "id_photo_url",fill FieldFill.UPDATE) private String idPhotoUrl; 方法一 Data EqualsAndHashCode(callSuper false) Accessors(chain true) TableName("base_party_member") A…

微软开源Visual Studio测试平台VSTest

IT之家1月21日消息 微软在MSDN博客上宣布&#xff0c;开源旗下Visual Studio测试平台VSTest。这一平台是具备高扩展性的单元测试执行框架&#xff0c;能够在不同的核心之间实现并行化&#xff0c;提供进程隔离&#xff0c;并能够整合进Visual Studio。 目前&#xff0c;VSTest能…