暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Spring事务的实现源码分析,以及事务不起作用原因分析

Java艺术 2021-09-08
390
关注“Java艺术”一起来充电吧!

这篇文章写完后我改了N次,反复的读,反复的改,为的是能让读者看懂。源码分析类的文章真的很难写,懂是一回事,写又是另一回事,能写给别人看得懂又是一回事。太多英文单词搞得排版有点乱。

本篇内容包括:
  • Spring注解事务的实现

  • mybatis-spring包为事务提供的支持

  • 动态数据源使用配置需要注意的问题

  • 动态数据源配置例子


事务不起作用原因有哪些?


我遇到过的就这两点:

  • 同一个bean中调用自身的添加事务注解的方法

  • 使用动态数据源配置不正确导致的


一个Service方法中直接调用另一个被声明事务的方法,因为是在this中调用的,就走不到事务的切面方法,也就直接导致事务不生效,对于此类问题,可以通过ApplicationContext获取Bean再调用,不要直接使用this调用。


关于第二点,使用动态数据源配置不正确导致的事务不起作用问题,我将留在文末分析,因为只有了解Spring事务的工作原理,才能真正的理解为什么会出现这样的问题。


Spring注解事务的实现


TransactionInterceptor是事务方法拦截器,或叫切面。TransactionInterceptor继承TransactionAspectSupport。本篇不分析Spring AOP部分的实现,只关注事务的实现。


当调用一个bean的被@Transaction注解注释的方法时,先走到TransactionInterceptor事务拦截器的invoke方法,因此事务拦截器的invoke方法就是分析注解事务实现的入口。


由于invoke方法调用invokeWithinTransaction,自身并不做任何事情,所以直接看invokeWithinTransaction方法。


TransactionInterceptor#invokeWithinTransaction方法】


invokeWithinTransaction方法整体分为四块

1、初始化事务支持,根据注解的属性配置,处理事务传播机制、为事务创建连接、为连接设置事务隔离级别等;

2、调用目标方法;
3、方法执行异常完成事务回滚;
4、方法执行成功完成事务提交。


源码分析涉及到的一些类说明:

spring-tx:

  • TransactionInterceptor(事务方法拦截器、切面)

  • PlatformTransactionManager(平台事务管理器)

  • TransactionStatus(事务状态)

  • ConnectionHolder(连接持有者)

  • TransactionSynchronizationManager(事务同步管理者


【一些关键对象的类图】


01




首先获取注解的属性配置信息,如事务隔离级别、是否只读事务、事务的传播机制、事务超时时间等(TransactionAttribute)。


接着获取事务管理器PlatformTransactionManager,如果@Transaction注解上指定了事务管理器则获取指定的事务管理器,否则使用默认的。一般我们只会注册一个事务管理器。除非配置了多数据源(非动态数据源),才会为每个数据源配置一个事务管理器。


初始化工作由createTransactionIfNecessary方法完成。




调用createTransactionIfNecessary方法创建事务(如果需要),IfNecessary说明并不一定会创建,比如当前已经存在一个事务,则根据事务的传播机制决定是否要创建。createTransactionIfNecessary方法返回一个TransactionInfo,TransactionInfo保存事务信息,如旧的事务的TransactionInfo、事务属性配置、事务管理器、当前事务方法的事务状态。


    protected final class TransactionInfo {
    // 事务管理器
    @Nullable
        private final PlatformTransactionManager transactionManager;
        // 事务注解的属性配置
    @Nullable
        private final TransactionAttribute transactionAttribute;
        // 切入点标志
        private final String joinpointIdentification;
        // 事务状态
    @Nullable
        private TransactionStatus transactionStatus;
        // 前一个事务的事务信息
    @Nullable
    private TransactionInfo oldTransactionInfo;
    }


    调用平台事务管理器PlatformTransactionManagergetTransaction方法为当前事务方法创建一个TransactionStatusTransactionStatus描述一个事务的状态,如该事务是否存在保存点、是否已完成等。


      public interface TransactionStatus extends SavepointManager {
        // 是否是新创建的事务
        boolean isNewTransaction();
        // 是否存在保存点
        boolean hasSavepoint();
        void setRollbackOnly();
        // 是否只回滚
        boolean isRollbackOnly();
        // 事务是否为已完成,即已提交或已回滚。
        boolean isCompleted();
      }



      接着分析PlatformTransactionManagergetTransaction方法,其实就是分析DataSourceTransactionManagergetTransaction方法



      在分析方法之前,先认识两个对象DataSourceTransactionObject与ConnectionHolder:


      DataSourceTransactionObject继承JdbcTransactionObjectSupport

        public abstract class JdbcTransactionObjectSupport 
                implements SavepointManagerSmartTransactionObject {
        // 持有数据库连接
        @Nullable
          private ConnectionHolder connectionHolder;
          // 之前的事务隔离级别,用于当事务退出时,还原Connection的事务隔离级别
        @Nullable
          private Integer previousIsolationLevel;
          // 是否允许使用保存点
        private boolean savepointAllowed = false;
         }


        DataSourceTransactionObject

           private static class DataSourceTransactionObject 
            extends JdbcTransactionObjectSupport {
                  // 持有的ConnectionHolder是否是新创建的
          private boolean newConnectionHolder;
                  // 事务结束时是否需要重置连接为自动提交
          private boolean mustRestoreAutoCommit;
           }


          ConnectionHolder

            public class ConnectionHolder extends ResourceHolderSupport {
            @Nullable
            private ConnectionHandle connectionHandle;
              // 当前数据库连接
            @Nullable
            private Connection currentConnection;
            // 当前事务状态
              private boolean transactionActive = false;
              // 是否支持保存点
            @Nullable
              private Boolean savepointsSupported;
              // 当前连接的事务的保存点总数
            private int savepointCounter = 0;
              }


            1、调用doGetTransaction创建事务对象DataSourceTransactionObject它继承JdbcTransactionObjectSupport实现的接口是SavepointManager,为当前事务提供创建保存点、回退到保存点、释放保存点继续执行的支持,而具体的创建保存点这些操作会交给ConnectionHolder完成的。DataSourceTransactionObject还会保存之前的事务隔离级别,用于当前事务退出时,还原Connection的事务隔离级别,否则当连接放入连接池被复用时就可能出现问题。



            如果当前已经存在一个事务,则根据自己持有的数据源从TransactionSynchronizationManager能拿到ConnectionHolder,如果当前未存在事务,则TransactionSynchronizationManagergetResource返回Null。如果拿到ConnectionHolder,则设置给DataSourceTransactionObject对象,并标志这不是一个新的连接。


            2、调用isExistingTransaction方法判断当前是否已经有事务存在了,如果当前存在事务,则调用handleExistingTransaction方法返回一个TransactionStatus,根据配置的事务传播机制处理。


              // 判断当前是否已经存在事务,其实是判断DataSourceTransactionObject
              // 是否已经从TransactionSynchronizationManager
              // 拿到了一个ConnectionHolder,且ConnectionHolder的transactionActive状态是否为true
              @Override
              protected boolean isExistingTransaction(Object transaction) {
              DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
              return (txObject.hasConnectionHolder() && txObject.getConnectionHolder().isTransactionActive());
              }


              这里会涉及到几种传播机制的处理,此处我只介绍其中的一种传播机制的处理。PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行;如果当前没有事务,则进行与PROPAGATION_REQUIRED类似的操作。


              handleExistingTransaction方法部分代码】

               case 1
              创建TransactionStatus为当前连接建保存点Savapoint。当方法执行过程中发生异常时,从TransactionStatus拿到保存点,回滚到保存点;当执行成功则移除保存点,回到上一个事务方法继续执行。
               case 2
              走正常的事务逻辑。

              3、如果不走步骤2,则当前不存在事务。当前方法创建一个TransactionStatus,并调用doBean方法doBean方法由子类DataSourceTransactionManager实现。



              doBean方法中判断当前DataSourceTransactionObject是否持有ConnectionHandler,如果有,则说明TransactionSynchronizationManager能拿到ConnectionHolder,已经存在一个事务了。如果没有,则从数据源中获取一个连接,并创建ConnectionHolder,赋值给DataSourceTransactionObject,并标志这是一个新创建的连接


              获取到连接之后,需要为当前事务对象,设置事务信息,分以下几步解析:

               step 1
              调用DataSourceUtils的prepareConnectionForTransaction方法为连接设置事务。如果当前事务属性配置为只读事务,则设置当前连接只能执行读操作;为获取到的连接设置数据库事务隔离级别。保存原来的隔离级别到事务对象DataSourceTransactionObject中,对于事务结束时恢复Connection的事务隔离级别。
               step 2
              如果当前连接是自动提交的,取消连接的自动提交,否则事务不生效。并且也要保存是否需要在事务结束时,将Connection的auto commit恢复为true。

               step 3
              调用自身的prepareTransactionalConnection方法,判断是否只读事务,如果是则执行一条sql为当前连接设置事务为只读事务。
               step 4
              设置当前事务对象持有的ConnectionHolder的事务状态为Active,标志Connection事务已经开启。
               step 5
              如果需要,为Connection设置事务超时时间。事务注解上的超时属性,如果是-1则不设置。

              step  6
              判断当前事务对象持有的ConnectionHolder是否是新创建的,如果是,则说明当前线程的方法调用栈上并未有任何方法开启事务,将ConnectionHolder绑定到TransactionSynchronizationManager的resources上,下次有事务进来就可能拿到这个ConnectionHolder

              TransactionSynchronizationManagerresources静态字段类型为ThreadLocal,所以同一个线程上的事务方法都能获取到同一个连接。用一个Map存储线程数据。如果是存储ConnectionHodler的,则Key为数据源DataSouce(如果使用动态数据源则为动态数据源)。后面分析SqlSession时,也是用这个字段存储的,所以Key定义为Object类型



              02



              完成事务的初始化工作之后,接着就是执行目标方法

                retVal = invocation.proceedWithInvocation();


                03



                如果方法执行出现异常,则执行回滚逻辑。

                1)、从TracsactionInfo中拿到事务注解的属性配置,判断@Transaction注解是否指定当遇到某种异常时才回滚,如果指定了,先判断当前异常类型是否匹配,如果不匹配,则走正常提交逻辑。
                2)、否则从事务信息TransactionInfo中获取事务状态TransactionStatus,调用事务管理器的rollback方法完成回滚。


                事务管理器的rollback方法分析:

                1)、根据事务状态TransactionStatus,判断当前事务是否有保存点Savepoint,如果有,则回滚到保存点,然后释放保存点。
                2)、否则如果是个新事务则整个事务回滚(要判断是否是新事务,因为事务的传播机制)。


                04



                否则如果方法执行正常,则执行提交逻辑。


                从事务信息TransactionInfo中获取事务状态TransactionStatus,调用事务管理器的commit方法完成提交。


                方法执行分析:
                1)、如果当前TracsactionStatus有保存点,则释放保存点;事务还不能提交,因为前一个事务方法被挂起了,还没有执行完成。
                2)、否则如果是个新事务,提交事务。

                mybatis-spring为事务提供的支持


                mybatis-spring-boot-autoconfigure包下的MybatisAutoConfiguration 会自动完成一些配置工作,如创建SqlSessionTemplate这个bean。


                源码分析涉及到的一些类说明

                mybatis-spring:

                • SqlSessionTemplateSqlSession的代理+委托)

                • SqlSessionInterceptorSqlSession代理拦截器)

                • SpringManagedTransactionmybatis-spring实现的mybatisTransaction

                • SpringManagedTransactionFactorymybatis-spring实现的mybatisTransactionFactory




                SqlSessionTemplate这是一个神奇的SqlSession,即有委托又有代理。SqlSessionTemplate也是实现SqlSession接口的,支持SqlSession的所有方法,同时它又不会去执行,而是交给一个SqlSession代理对象去执行,这个代理对象是在SqlSessionTemplate的构造方法中创建的,使用jdk动态代理。而InvocationHandler正是SqlSessionInterceptor


                  public class SqlSessionTemplate implements SqlSession,    
                  DisposableBean {


                    private final SqlSessionFactory sqlSessionFactory;
                    // 执行器类型,通过解析Mapper标签的<insert>、<update>、<delete>、<select>标签获得
                    private final ExecutorType executorType;
                    // SqlSession代理类,jdk动态代理创建
                    private final SqlSession sqlSessionProxy;
                  private final PersistenceExceptionTranslator exceptionTranslator;


                  public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType) {
                  this(sqlSessionFactory, executorType,
                  new MyBatisExceptionTranslator(
                  sqlSessionFactory.getConfiguration().getEnvironment().getDataSource(), true));
                  }

                  public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
                  PersistenceExceptionTranslator exceptionTranslator) {


                  this.sqlSessionFactory = sqlSessionFactory;
                  this.executorType = executorType;
                  this.exceptionTranslator = exceptionTranslator;
                       // 创建代理类
                  this.sqlSessionProxy = (SqlSession) newProxyInstance(
                  SqlSessionFactory.class.getClassLoader(),
                  new Class[] { SqlSession.class },
                  new SqlSessionInterceptor());
                  }


                  }


                  Mapper接口中的每个方法都会通过解析对应Mapper配置文件的标签生成一个MapperMethod解析完所有方法之后,会为每个Mapper接口都通过JDK动态代理生成一个MapperProxy对象。


                    public MapperProxy(
                    // SqlSessionTemplate
                    SqlSession sqlSession, 
                    // Mapper接口
                    Class<TmapperInterface
                                 // 反射获取的MethodMapperMethod映射
                    Map<MethodMapperMethodmethodCache){
                    }


                    当调用Mapper接口的方法时,都会进入MapperProxyinvoke方法.

                      public Object invoke(Object proxy, Method method, Object[] args){
                      }


                      通过method获取对应的MapperMethod,调用execute方法执行。

                        org.apache.ibatis.binding.MapperMethod#execute
                        public Object execute(SqlSession sqlSession, Object[] args){


                        }


                        MapperMethodexecute方法通过命令类型(insert、update、delete、select)调用SqlSessioninsert、update、delete、selectList、selectOne方法。而此时的SqlSession正是单例的SqlSessionTemplate



                        调用SqlSessionTemplate的方法会进入SqlSessionInterceptorinvoke方法(JDK动态代理,SqlSessionInterceptor是InvocationHandler)。此时才会真正的去获取一个SqlSession



                        getSqlSession方法的执行步骤分析:



                        1、从TransactionSynchronizationManager中获取当前是否持有一个SqlSessionHolder,如果有则判断这个SqlSessionHolderExecutorType(CRUD是否相同,相同才使用这个SqlSession。否则调用SqlSessionFactoryopenSession方法获取一个SqlSession


                        这个TransactionSynchronizationManager前面分析事务的时候已经很熟悉了,它用于存储当前线程数据,而且都是使用resources这个静态字段存储的,这是一个ThreadLocal


                        • 当key的类型为数据源时,存储的就是当前事务的连接持有者ConnectionHodler

                        • 当key的类型为SqlSession工厂时,存储的就是当前SqlSession持有者SqlSessionHodler


                        可能还存在其它的,只是当前我发现的就这两种。所以resources的命名由此而来,也正是resource静态字段配置ThreadLocal的泛型类型为Map的原因。


                        为什么不使用多个ThreadLocal存储的?如果用多个ThreadLocal存储,容易由于疏忽忘了调用哪个ThreadLocalremove导致内存泄露问题。如果resources不仅仅只是保持ConnectionHodlerSqlSession呢,使用多个ThreadLocal难以管理,最重要的就是代码难以阅读,会显得很乱。这是我的个人观察,看源码就是多吸收一些优秀的编程思想。


                        SqlSession的创建在SqlSessionFactoryopenSessionFromDataSource方法中完成的。



                        先调用SpringManagedTransactionFactory对象的newTransaction创建一个SpringManagedTransactionmybatisTransaction接口的实现类,用于SpringMybatis整合提供事务的支持),再创建Executor,最后创建DefaultSqlSession

                          // 三者的关系
                          DefaultSqlSession-> 调用Executor执行方法 -> 调用SpringManagedTransaction获取连接


                          newTransaction方法传入的数据源是配置SqlSessionFactory注入的数据源,所以SqlSessionFactoryPlatformTransactionManager配置的数据源要相同,否则事务获取的连接与实际执行Sql获取到的连接将会不同。


                          2、将创建的SqlSession放到SqlSessionHolder中,将其绑定到TransactionSynchronizationManagerThreadLocal)。一个事务中,如果执行的命令都是同一种类型,如update,则只会创建一个DefaultSqlSession。如果已经持有一个DefaultSqlSession则会直接使用。



                          DefaultSqlSession就是mybatis实现的SqlSession,所以往下走就是Mybatis的调用流程。在执行方法过程中,会调用ExecutorgetConnection方法获取连接,而ExecutorgetConnection方法会调用TransactiongetConnection方法。

                          这个Transaction就是SpringManagedTransaction。所以一个SqlSession不等于一个数据库连接。Mybatis中执行SQL之前再通过Transaction获取连接。



                          SpringManagedTransactiongetConnection方法如果当前已经打开一个连接,则返回当前连接,否则调用DataSourceUtils工具类的getConnection方法获取连接,而getConnection方法传递的参数是当前SpringManagedTransaction对象持有的数据源。根据数据源从TransactionSynchronizationManager中看看当前是否有事务打开了连接,如果有,则从TransactionSynchronizationManager拿到当前事务使用的连接。



                          如果能从TransactionSynchronizationManager中拿到连接,则说明当前执行的Mapper方法在事务中。如果拿不到则直接从数据源中获取一个连接,也就是走的非事务逻辑了。


                          动态数据源使用配置需要注意的问题


                          如果配置的PlatformTransactionManager事务管理器,使用的是一个动态数据源,那么TransactionSynchronizationManager会将这个动态数据源作为Key,因为多个数据源都是被动态数据源管理的,所以动态数据源内部怎么切换,一个事务中TransactionSynchronizationManager持有的都是第一次获取到的连接,整个事务中都会使用这个连接,因此在事务中切换数据源无效,应在事务之前完成数据源的切换。


                          SqlSessionFactoryPlatformTransactionManager配置的数据源一定要相同!SqlSessionFactoryPlatformTransactionManager配置的数据源一定要相同!SqlSessionFactoryPlatformTransactionManager配置的数据源一定要相同!


                          TransactionSynchronizationManager通过数据源作为key缓存事务持有的ConnectionHodler,PlatformTransactionManager创建。SpringManagedTransaction是在DefaultSqlSessionFactoryopenSession方法中调用SpringManagedTransactionFactory传入数据源创建的。因此,如果SqlSessionFactoryPlatformTransactionManager配置的数据源不同ConnectionHodler所用数据源,将会与SpringManagedTransaction使用的数据源不同ConnectionHodler所持有的连接就不会被Mybatis所使用,事务就不会生效。


                          动态数据源配置例子


                          示例一:

                               @Bean(name = DsConstant.DEFAULT_MYSQL_DB_01)
                                public DataSource mysqlDatabase01() {
                            DruidDataSource druidDataSource = new DruidDataSource();
                            ......
                            return druidDataSource;
                            }

                            @Bean(name = DsConstant.DEFAULT_MYSQL_DB_02)
                            public DataSource mysqlDatabase02() {
                            DruidDataSource druidDataSource = new DruidDataSource();
                            ......
                            return druidDataSource;
                            }

                            /**
                                 * 动态数据源
                            *
                            * @return
                            */
                                @Bean(name = DsConstant.DS)
                                public DynamicDataSource dynamicDataSource(
                                 @Qualifier(DsConstant.DEFAULT_MYSQL_DB_01) DataSource db1,
                            @Qualifier(DsConstant.DEFAULT_MYSQL_DB_02) DataSource db2) {
                            DynamicDataSource ds = new DynamicDataSource();
                                    ......
                            return ds;
                                }
                                
                                /**
                                 *  SqlSessionFactory配置
                                 */
                                @Bean
                                public SqlSessionFactoryBean platformSqlSessionFactory(@Qualifier(DsConstant.DS) DataSource dynamicDataSource,
                                                                                       @Autowired MybatisProperties mybatisProperties) {
                            SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
                            // 配置数据源
                                    sqlSessionFactoryBean.setDataSource(dataSource);
                                    PathMatchingResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
                                    // mapper文件位置
                                    sqlSessionFactoryBean.setMapperLocations(mybatisProperties.resolveMapperLocations());
                            sqlSessionFactoryBean.setConfigLocation(resourcePatternResolver.getResource(mybatisProperties.getConfigLocation()));
                            // 使用mybatis-spring提供的事务工厂
                            TransactionFactory transactionFactory = new SpringManagedTransactionFactory();
                            sqlSessionFactoryBean.setTransactionFactory(transactionFactory);
                            return sqlSessionFactoryBean;
                            }
                                
                            /**
                            * 事务管理者
                            *
                            * @param platformDataSource
                            * @return
                            */
                            @Bean
                                public PlatformTransactionManager mysqlPlatformTransactionManager(@Qualifier(DsConstant.DS) DynamicDataSource dynamicDataSource) {
                            return new DataSourceTransactionManager(dynamicDataSource);
                            }



                            示例二:

                                 @Bean(name = DsConstant.DEFAULT_MYSQL_DB_01)
                              public DataSource mysqlDatabase01() {
                              DruidDataSource druidDataSource = new DruidDataSource();
                              ......
                              return druidDataSource;
                              }

                              @Bean(name = DsConstant.DEFAULT_MYSQL_DB_02)
                              public DataSource mysqlDatabase02() {
                              DruidDataSource druidDataSource = new DruidDataSource();
                              ......
                              return druidDataSource;
                              }

                              /**
                              * 动态数据源
                              *
                                   *  @Primary注解:声明这个bean为同类型bean的高优先级bean,
                                   *   当自动注入不指定bean的name时,且存在多个同类型的bean时,
                                   *   自动注入会优先使用这个bean
                                   */
                              @Primary
                              @Bean(name = DsConstant.DS)
                              public DynamicDataSource dynamicDataSource(
                              @Qualifier(DsConstant.DEFAULT_MYSQL_DB_01) DataSource db1,
                              @Qualifier(DsConstant.DEFAULT_MYSQL_DB_02) DataSource db2) {
                              DynamicDataSource ds = new DynamicDataSource();
                              ......
                              return ds;
                              }
                                  
                              /**
                              * 事务管理者
                              *
                              * @param platformDataSource
                              * @return
                              */
                              @Bean
                              public PlatformTransactionManager mysqlPlatformTransactionManager(@Qualifier(DsConstant.DS) DynamicDataSource dynamicDataSource) {
                              return new DataSourceTransactionManager(dynamicDataSource);
                              }


                              示例二主要借助@Primary注解与mybatis-spring-boot-autoconfigure包下的MybatisAutoConfiguration自动配置类,实现自动配置SqlSessionFactory


                                @org.springframework.context.annotation.Configuration
                                // 条件注入:存在指定的类型才注入这个配置类
                                @ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class })
                                // 条件注入:容器中存在数据源才注入这个配置类
                                @ConditionalOnBean(DataSource.class)
                                @EnableConfigurationProperties(MybatisProperties.class)
                                // 自动配置在DataSourceAutoConfiguration完成之后
                                // 如果项目中自己实现配置,则需要导出DataSourceAutoConfiguration这个自动配置
                                @AutoConfigureAfter(DataSourceAutoConfiguration.class)
                                public class MybatisAutoConfiguration {
                                .....
                                 // 当前未注册SqlSessionFactory,则自动创建SqlSessionFactory
                                @Bean
                                 @ConditionalOnMissingBean
                                public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
                                }

                                  // SqlSessionTemplate也是由此自动创建的
                                  @Bean
                                  @ConditionalOnMissingBean
                                public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
                                }
                                ....
                                }


                                试一试,配置一个动态数据源,给动态数据源分配两个都是指向同一个数据库的数据源,然后配置SqlSessionFactoryBeanSqlSessionFactory)使用一个非动态数据源,而给PlatformTransactionManager事务管理器使用这个动态数据源,写一个事务例子,看看会发生什么?



                                公众号:Java艺术
                                扫码关注最新动态


                                文章转载自Java艺术,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                                评论