您的位置:首页 > 编程语言 > Java开发

Spring + Jta +JDBCTemplate 分布式事物实现方式

2017-07-31 16:41 435 查看
最近项目中需要用到多数据源管,数据访问层采用的是JDBCTemplate去做的,一开始是在数据源这块做了一个多数据源的操作类,通过拦截器注解去动态赋值指定的数据源操作,这种做法在查询中是没有问题的,但是DML操作时,会出现问题:事物中无法动态操作数据源,导致很多操作指针对第一个库。查询资料的时候发现:

DataSourceTransactionManager这个事物管理类只针对单个数据源进行事物控制.

解决的方案也有多种,这里只提及我实践过的两种:

开启多个数据源去进行操作,这种是可以自己去实现的.

利用JTA和Atomikos的多数据源分布事物管理

方案一:

思路:

- 自定义一个注解类,通过传递参数告诉这个业务需要用到哪几个数据源

- 然后仿照Spring中的@Transactional的实现模式,去构建一个集合来开启多个事物

- 然后通过拦截器去动态分配业务给这个集合告诉他,要开几个事物

代码:

applicationContext-datasource.xml

<!-- 数据源1 -->
<bean id="mvp" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
<property name="driverClassName" value="${mvp.driverClassName}"></property>
<property name="url" value="${mvp.url}"></property>
<property name="username" value="${mvp.username}"></property>
<property name="password" value="${mvp.password}"></property>
<property name="filters" value="${mvp.filters}"/>
<!-- 配置初始化大小、最小、最大 -->
<property name="initialSize" value="${mvp.initialSize}"/>
<property name="minIdle" value="${mvp.minIdle}"/>
<property name="maxActive" value="${mvp.maxActive}"/>
<!-- 配置获取连接等待超时的时间 -->
<property name="maxWait" value="${mvp.maxWait}"/>
<!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
<property name="timeBetweenEvictionRunsMillis" value="${mvp.timeBetweenEvictionRunsMillis}"/>
<!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
<property name="minEvictableIdleTimeMillis" value="${mvp.minEvictableIdleTimeMillis}"/>
<property name="testWhileIdle" value="${mvp.testWhileIdle}"/>
<property name="testOnBorrow" value="${mvp.testOnBorrow}"/>
<property name="testOnReturn" value="${mvp.testOnReturn}"/>
<!-- 打开PSCache,并且指定每个连接上PSCache的大小 -->
<property name="poolPreparedStatements" value="${mvp.poolPreparedStatements}"/>
<property name="maxPoolPreparedStatementPerConnectionSize" value="20"/>
<property name="removeAbandoned" value="${mvp.removeAbandoned}" />
<property name="removeAbandonedTimeout" value="${mvp.removeAbandonedTimeout}" />
<property name="maxOpenPreparedStatements" value="${mvp.maxOpenPreparedStatements}" />
<property name="logAbandoned" value="${mvp.logAbandoned}"/>
<property name="queryTimeout" value="${mvp.querytimeout}"/>
<property name="validationQuery" value="${mvp.validationQuery}"/>
</bean>

<!-- 数据源2 -->
<bean id="system" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
<property name="driverClassName" value="${system.driverClassName}"></property>
<property name="url" value="${system.url}"></property>
<property name="username" value="${system.username}"></property>
<property name="password" value="${system.password}"></property>
<property name="filters" value="${system.filters}"/>
<!-- 配置初始化大小、最小、最大 -->
<property name="initialSize" value="${system.initialSize}"/>
<property name="minIdle" value="${system.minIdle}"/>
<property name="maxActive" value="${system.maxActive}"/>
<!-- 配置获取连接等待超时的时间 -->
<property name="maxWait" value="${system.maxWait}"/>
<!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
<property name="timeBetweenEvictionRunsMillis" value="${system.timeBetweenEvictionRunsMillis}"/>
<!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
<property name="minEvictableIdleTimeMillis" value="${system.minEvictableIdleTimeMillis}"/>
<property name="testWhileIdle" value="${system.testWhileIdle}"/>
<property name="testOnBorrow" value="${system.testOnBorrow}"/>
<property name="testOnReturn" value="${system.testOnReturn}"/>
<!-- 打开PSCache,并且指定每个连接上PSCache的大小 -->
<property name="poolPreparedStatements" value="${system.poolPreparedStatements}"/>
<property name="maxPoolPreparedStatementPerConnectionSize" value="20"/>
<property name="removeAbandoned" value="${system.removeAbandoned}" />
<property name="removeAbandonedTimeout" value="${system.removeAbandonedTimeout}" />
<property name="maxOpenPreparedStatements" value="${system.maxOpenPreparedStatements}" />
<property name="logAbandoned" value="${system.logAbandoned}"/>
<property name="queryTimeout" value="${system.querytimeout}"/>
<property name="validationQuery" value="${system.validationQuery}"/>
</bean>

<!-- dao层访问处理工具 分别指定两个不同的datasource -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="default"/>
</bean>

<bean id="jdbcTemplate2" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="system"/>
</bean>


applicationContext-service.xml 业务层配置文件

<!-- 一些扫描包或者其他定义的业务类 我这边就不列入了 -->

<!-- 多数据源切面类 -->
<bean id="multiTransactionalAspect" class="com.elab.execute.utils.MultiTransactionalAspect"/>
多数据Aop配置
<aop:config proxy-target-class="true">
<!-- 定义一个切入点表达式: 拦截哪些方法 -->
<aop:aspect ref="multiTransactionalAspect">
<aop:pointcut id="pointUserMgr" expression="execution(* com.test.execute.services..*(..))"/>
<aop:around method="around" pointcut-ref="pointUserMgr" />
</aop:aspect>
</aop:config>


注解类 :

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface MultiTransactional {

String[] values() default "";

// TODO 当然你如果想做的更完善一点,可以参考@Transactional这个类,自己去做,什么传播机制啊,隔离级别啊 等等,思路是一样的
}


注解实现类 - 其实这里差不多都是沿用Spring的事物实现方式:

import com.alibaba.druid.pool.DruidDataSource;
import org.apache.commons.lang3.ArrayUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.stereotype.Component;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;

import java.lang.reflect.Method;
import java.util.Stack;

/**
* @Description: 多数据源动态管理
* @Author: Liukx on 2017/7/28 - 16:41
*/
@Component("multiTransactionalAspect")
public class MultiTransactionalAspect {

private Logger logger = LoggerFactory.getLogger(getClass());

@Autowired
private SpringUtils springUtils;

/**
* 切入点
*
* @param point
* @return
* @throws Throwable
*/
public Object around(ProceedingJoinPoint point) throws Throwable {
Stack<DataSourceTransactionManager> dataSourceTransactionManagerStack = new Stack<DataSourceTransactionManager>();
Stack<TransactionStatus> transactionStatuStack = new Stack<TransactionStatus>();

try {
Object target = point.getTarget();
String method = point.getSignature().getName();
Class classz = target.getClass();
Class[] parameterTypes = ((MethodSignature) point.getSignature()).getMethod().getParameterTypes();
Method m = classz.getMethod(method, parameterTypes);
if (m != null && m.isAnnotationPresent(MultiTransactional.class)) {
MultiTransactional multiTransactional = m.getAnnotation(MultiTransactional.class);
if (!openTransaction(dataSourceTransactionManagerStack, transactionStatuStack, multiTransactional)) {
return null;
}
Object ret = point.proceed();
commit(dataSourceTransactionManagerStack, transactionStatuStack);
return ret;
}
Object ret = point.proceed();
return ret;
} catch (Exception e) {
rollback(dataSourceTransactionManagerStack, transactionStatuStack);
logger.error(String.format(
"MultiTransactionalAspect, method:%s-%s occors error:", point
.getTarget().getClass().getSimpleName(), point
.getSignature().getName()), e);
throw e;
}
}

/**
* 打开一个事物方法
*
* @param dataSourceTransactionManagerStack
* @param transactionStatuStack
* @param multiTransactional
* @return
*/
private boolean openTransaction(
Stack<DataSourceTransactionManager> dataSourceTransactionManagerStack,
Stack<TransactionStatus> transactionStatuStack,
MultiTransactional multiTransactional) {
// 获取注解中要打开的事物类型
String[] transactionMangerNames = multiTransactional.values();
if (ArrayUtils.isEmpty(multiTransactional.values())) {
return false;
}

for (String beanName : transactionMangerNames) {
// 创建一个新的事物管理器,用来管理接下来要用到的事物
DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
// 根据注解中获取到的数据标识,从spring容器中去查找对应的数据源
DruidDataSource dataSource = (DruidDataSource) springUtils.getBean(beanName);
//然后交给事物管理器去管理
dataSourceTransactionManager.setDataSource(dataSource);
// 定义一个新的事物定义
DefaultTransactionDefinition defaultTransactionDefinition = new DefaultTransactionDefinition();
// 设置一个默认的事物传播机制,注意的是这里可以拓展注解中没有用到的属性
// defaultTransactionDefinition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(defaultTransactionDefinition);
// 将这个事物的定义放入Stack中
/**
* 其中为什么要用Stack来保存TransactionManager和TransactionStatus呢?
* 那是因为Spring的事务处理是按照LIFO/stack behavior的方式进行的。
* 如若顺序有误,则会报错:
*/
transactionStatuStack.push(transactionStatus);
dataSourceTransactionManagerStack
.push(dataSourceTransactionManager);
}
return true;
}

/**
* 提交事物方法实现
*
* @param dataSourceTransactionManagerStack
* @param transactionStatuStack
*/
private void commit(
Stack<DataSourceTransactionManager> dataSourceTransactionManagerStack,
Stack<TransactionStatus> transactionStatuStack) {
while (!dataSourceTransactionManagerStack.isEmpty()) {
dataSourceTransactionManagerStack.pop().commit(
transactionStatuStack.pop());
}
}

/**
* 回滚事物方法实现
*
* @param dataSourceTransactionManagerStack
* @param transactionStatuStack
*/
private void rollback(
Stack<DataSourceTransactionManager> dataSourceTransactionManagerStack,
Stack<TransactionStatus> transactionStatuStack) {
while (!dataSourceTransactionManagerStack.isEmpty()) {
dataSourceTransactionManagerStack.pop().rollback(
transactionStatuStack.pop());
}
}
}


service 1 - 2 :

/**
* 一些实验性Demo接口
*
* @author Liukx
* @create 2017-07-28 10:11
* @email liukx@elab-plus.com
**/
public interface IDemoService {

public void mvpManage() throws Exception;

}

/**
* 一些实验性Demo接口
*
* @author Liukx
* @create 2017-07-28 10:11
* @email liukx@elab-plus.com
**/
public interface IDemoService2 {

public void beidouMange() throws CoreException;

}


service实现类:

/**
* 实现性Demo接口实现
*
* @author Liukx
* @create 2017-07-28 10:12
* @email liukx@elab-plus.com
**/
@Service("demoService")
public class DemoServiceImpl implements IDemoService {

@Autowired
@Qualifier("demoTestDao")
private IDemoTestDao testDao;

@Autowired
private IDemoService2 demoService2;

//    @Autowired
//    private DataSourceTransactionManager transactionManager;

//TODO 这个注解很关键 - 必须是这个注解才能被拦截器拦截到
@MultiTransactional(values = {DataSource.mvp, DataSource.system})
public void mvpManage() throws CoreException {
System.out.println("=====================开始处理MVP");
testDao.insert();
List<Map<String, Object>> select = testDao.select();
System.out.println("===============>>>>>>>>>>>>>>>>>" + select.size());
System.out.println("=====================结束处理MVP");

demoService2.beidouMange();

//        System.out.println("业务处理完成,开始报错");
int i = 1 / 0;
}

}


service 2实现

/**
* @author Liukx
* @create 2017-07-28 11:07
* @email liukx@elab-plus.com
**/
@Service("demo2")
public class DemoServiceImpl2 implements IDemoService2 {
@Autowired
@Qualifier("beidouTestDao")
private IDemoTestDao testDao;

@MultiTransactional(values = DataSource.system)
public void beidouMange() throws CoreException {
System.out.println("=====================开始处理北斗");
testDao.insert();
List<Map<String, Object>> select = testDao.select();
System.out.println("========================================>" + select.size());
System.out.println("=====================结束处理北斗");
}
}


dao实现: 这里就只列入实现类

@Repository("beidouTestDao")
public class BeiDouTestDaoImpl extends BaseDao implements IDemoTestDao {

/**
* 我们这块目前是有一些封装的,不过你不用太关注;
* 你只要把对应的数据源指定好就行了
*/
@Autowired
@Qualifier("jdbcTemplate2")
public JdbcTemplate jdbcTemplate;

public int insert() throws CoreException {
LinkedHashMap<String, Object> params = new LinkedHashMap<>();
params.put("name", "某某某" + RandomUtils.randomNum(3));
params.put("created", new Date());
return executeInsert("test.insert", params);
}

@Override
public List<Map<String, Object>> select() throws CoreException {
return findList("test.findAll", new LinkedHashMap<String, Object>());
}
}


@Repository("demoTestDao")
public class DemoTestDaoImpl extends BaseDao implements IDemoTestDao {
/**
* 我们这块目前是有一些封装的,不过你不用太关注;
* 你只要把对应的数据源指定好就行了
*/
@Autowired
@Qualifier("jdbcTemplate")
public JdbcTemplate jdbcTemplate;

public int insert() throws CoreException {
LinkedHashMap<String, Object> params = new LinkedHashMap<>();
params.put("name", "某某某" + RandomUtils.randomNum(2));
params.put("created", new Date());
return executeInsert("test.insert", params);
}

public List<Map<String, Object>> select() throws CoreException {
return findList("test.findAll", new LinkedHashMap<String, Object>());
}
}


另外有一个动态获取bean的工具类:

@Component
public class SpringUtils implements ApplicationContextAware {
private ApplicationContext applicationContext;

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}

public Object getBean(String beanId) {
return applicationContext.getBean(beanId);
}
}


PS : 这个方案会存在一定的问题 -> 就是如果你下一个另一个数据库操作的业务serivce方法中如果定义了@Transactional它将会又开启一个事物去执行…

方案2 : 利用JTA+Atomikios实现事物管理,业务层代码和上面差不多,主要是配置文件这块.

applicationContext-datasource.xml

<!-- 父配置 -->
<bean id="abstractXADataSource" class="com.atomikos.jdbc.AtomikosDataSourceBean" init-method="init"
destroy-method="close" abstract="true">
<property name="xaDataSourceClassName"
value="com.alibaba.druid.pool.xa.DruidXADataSource"/>  <!-- SQLErrorCodes loaded: [DB2, Derby, H2, HSQL, Informix, MS-SQL, MySQL, Oracle, PostgreSQL, Sybase, Hana]  -->
<property name="poolSize" value="10"/>
<property name="minPoolSize" value="10"/>
<property name="maxPoolSize" value="30"/>
<property name="borrowConnectionTimeout" value="60"/>
<property name="reapTimeout" value="20"/>
<property name="maxIdleTime" value="60"/>
<property name="maintenanceInterval" value="60"/>
<property name="loginTimeout" value="60"/>
<property name="testQuery" value="${default.validationQuery}"/>
</bean>

<!-- 主配置 -->
<bean id="masterDataSource" parent="abstractXADataSource">
<property name="uniqueResourceName" value="masterDB"/>
<property name="xaProperties">
<props>
<prop key="driverClassName">${default.driverClassName}</prop>
<prop key="url">${default.url}</prop>
<prop key="password">${default.password}</prop>
<!--  <prop key="user">${jdbc.username}</prop> --> <!-- mysql -->
<prop key="username">${default.username}</prop>   <!-- durid -->
<prop key="initialSize">0</prop>
<prop key="maxActive">20</prop> <!-- 若不配置则代码执行"{dataSource-1} inited"此处停止  -->
<prop key="minIdle">0</prop>
<prop key="maxWait">60000</prop>
<prop key="validationQuery">${default.validationQuery}</prop>
<prop key="testOnBorrow">false</prop>
<prop key="testOnReturn">false</prop>
<prop key="testWhileIdle">true</prop>
<prop key="removeAbandoned">true</prop>
<prop key="removeAbandonedTimeout">1800</prop>
<prop key="logAbandoned">true</prop>
<prop key="filters">mergeStat</prop>
</props>
</property>
</bean>

<bean id="slaveDataSource" parent="abstractXADataSource">
<property name="uniqueResourceName" value="slaveDB"/>
<property name="xaProperties">
<props>
<prop key="driverClassName">${system.driverClassName}</prop>
<prop key="url">${system.url}</prop>
<prop key="password">${system.password}</prop>
<!--  <prop key="user">${jdbc.username}</prop> -->
<prop key="username">${system.username}</prop>
<prop key="initialSize">0</prop>
<prop key="maxActive">20</prop>
<prop key="minIdle">0</prop>
<prop key="maxWait">60000</prop>
<prop key="validationQuery">${system.validationQuery}</prop>
<prop key="testOnBorrow">false</prop>
<prop key="testOnReturn">false</prop>
<prop key="testWhileIdle">true</prop>
<prop key="removeAbandoned">true</prop>
<prop key="removeAbandonedTimeout">1800</prop>
<prop key="logAbandoned">true</prop>
<prop key="filters">mergeStat</prop>
</props>
</property>
</bean>

<!-- 将数据源交给数据库操作模版工具 -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="masterDataSource"/>
</bean>

<bean id="jdbcTemplate2" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="slaveDataSource"/>
</bean>


applicationContext-atomikos.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd" default-lazy-init="true">

<description>配置事物</description>
<!-- atomikos事务管理器 -->
<bean id="atomikosTransactionManager" class="com.atomikos.icatch.jta.UserTransactionManager" init-method="init"
destroy-method="close">
<property name="forceShutdown">
<value>true</value>
</property>
</bean>

<bean id="atomikosUserTransaction" class="com.atomikos.icatch.jta.UserTransactionImp">
<property name="transactionTimeout" value="300"/>
</bean>
<!-- spring 事务管理器 -->
<bean id="springTransactionManager" class="org.springframework.transaction.jta.JtaTransactionManager">
<property name="transactionManager" ref="atomikosTransactionManager"/>
<property name="userTransaction" ref="atomikosUserTransaction"/>
<!-- 必须设置,否则程序出现异常 JtaTransactionManager does not support custom isolation levels by default -->
<property name="allowCustomIsolationLevels" value="true"/>
</bean>

<aop:config proxy-target-class="true">
<aop:pointcut id="pointUserMgr" expression="execution(* com.elab.execute.services..*(..))"/>
<aop:advisor pointcut-ref="pointUserMgr" advice-ref="txAdvice"/>
</aop:config>
<!-- 通过切入点去实现事物触发机制 -->
<tx:advice id="txAdvice" transaction-manager="springTransactionManager">
<tx:attributes>
<tx:method name="get*" propagation="REQUIRED" read-only="true"/>
<tx:method name="find*" propagation="REQUIRED" read-only="true"/>
<tx:method name="has*" propagation="REQUIRED" read-only="true"/>
<tx:method name="locate*" propagation="REQUIRED" read-only="true"/>
<tx:method name="mvp*" propagation="REQUIRED" rollback-for="java.lang.Exception"/>
<tx:method name="beidou*" propagation="REQUIRED" rollback-for="java.lang.Exception"/>
</tx:attributes>
</tx:advice>
<!--<tx:annotation-driven transaction-manager="springTransactionManager"-->
<!--proxy-target-class="true"></tx:annotation-driven>-->
</beans>


pom.xml

<!-- JTA -->
<dependency>
<groupId>javax.transaction</groupId>
<artifactId>jta</artifactId>
<version>1.1</version>
</dependency>
<dependency>
<groupId>com.atomikos</groupId>
<artifactId>atomikos-util</artifactId>
<version>4.0.2</version>
</dependency>
<dependency>
<groupId>com.atomikos</groupId>
<artifactId>transactions</artifactId>
<version>4.0.2</version>
</dependency>
<dependency>
<groupId>com.atomikos</groupId>
<artifactId>transactions-jta</artifactId>
<version>4.0.2</version>
</dependency>
<dependency>
<groupId>com.atomikos</groupId>
<artifactId>transactions-jdbc</artifactId>
<version>4.0.2</version>
</dependency>
<dependency>
<groupId>com.atomikos</groupId>
<artifactId>transactions-api</artifactId>
<version>4.0.2</version>
</dependency>
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib-nodep</artifactId>
<version>3.2.2</version>
</dependency>


业务层代码和方案一的差不多,看了一下大概的思路:

1. 在调用被拦截器匹配的方法时,开启一个新的事物

2. 当执行到DML操作时,会获取他对应的数据源,并且会和当前线程的事物管理器中的数据源进行匹配,如果不存在,则到连接池中获取一个新的连接,并把这个连接放入当前线程的事物管理器中进行管理

3. 当所有业务执行完毕,并且没有报错的时候,会执行一个两阶段提交的方式

PREPARE TRANSACTION transaction_id PREPARE TRANSACTION 为当前事务的两阶段提交做准备。 在命令之后,事务就不再和当前会话关联了;它的状态完全保存在磁盘上, 它提交成功有非常高的可能性,即使是在请求提交之前数据库发生了崩溃也如此。这条命令必须在一个用BEGIN显式开始的事务块里面使用。
COMMIT PREPARED transaction_id 提交已进入准备阶段的ID为transaction_id的事务
ROLLBACK PREPARED transaction_id 回滚已进入准备阶段的ID为transaction_id的事务


推荐使用第二种方式,因为它有比较好的连接池以及相对完善的机制,第一种考虑的情况比较少,会出现问题,当然,你如果愿意折腾自己写一套,可以参考一下..

以上为个人学习参考,有什么不足的地方欢迎指正.

参考博客:

Mybatis + JTA http://blog.csdn.net/zmx729618/article/details/54344296

分布式事物提交以及JTA的概念 : http://www.jasongj.com/big_data/two_phase_commit/

JTA一些实现原理:https://www.ibm.com/developerworks/cn/java/j-lo-jta/
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息