分布式事务是一个复杂的问题,上篇讲了分布式事务常用的几种解决方案,其实最常用的是消息最终一致性方案,rocketMQ中的事务也是使用消息最终一致性方案的思路来实现的。
rocketMQ保证本地事务成功时,消息一定会发送成功并被成功消费,如果本地事务失败了,消息就不会被发送。
下边引用一张rocketMQ官网的事物处理流程图:
首先介绍下上图中提到的2个概念
half消息是什么?
half消息指的是暂时无法投递的消息。当消息成功发送到MQ服务器,但服务器没有收到来自生产者的消息的第二次确认时,该消息被标记为“暂时无法投递”。此状态中的消息称为half消息。
消息状态回查是什么意思?
由于网络断开或生产者应用程序重新启动可能导致丢失事务消息的第二次确认。当MQ服务器发现消息状态长时间为half消息时,它将向消息生产者发送回查请求,检查生产者上次本地事物的执行结果,以此为基础进行消息的提交或回滚。
上边的图详细描述了rocketMQ中分布式事务的每个阶段,下边用文字描述一下这个过程:
生产者向MQ服务器发送half消息。
half消息发送成功后,MQ服务器返回确认消息给生产者。
生产者开始执行本地事务。
根据本地事务执行的结果(UNKNOW、commit、rollback)向MQ Server发送提交或回滚消息。
如果错过了(可能因为网络异常、生产者突然宕机等导致的异常情况)提交/回滚消息,则MQ服务器将向同一组中的每个生产者发送回查消息以获取事务状态。
回查生产者本地事物状态。
生产者根据本地事务状态发送提交/回滚消息。
MQ服务器将丢弃回滚的消息,但已提交(进行过二次确认的half消息)的消息将投递给消费者进行消费。
从上述流程可以知道,事务消息其实只是保证了生产者本地事务和发送消息的原子性
消费者在消费事务消息时,broker处理事务消息的消费与普通消息是一样的,若消费不成功,则broker会重复投递该消息16次,若仍然不成功则需要人工介入。
OK,知道了rocketMQ的事物处理流程后,我们根据官网提供的例子跟下源码再看看(rocketMQ版本为4.3.0)
一、生产者发送prepare消息
客户端发送事务消息的部分(完整代码请查看org.apache.rocketmq.example.transaction.TransactionProducer)
生产者TransactionProducer代码如下:
public class TransactionProducer { public static void main(String[] args) throws MQClientException, InterruptedException { //本地事务回调组件 TransactionListener transactionListener = new TransactionListenerImpl(); //生产者初始化 TransactionMQProducer producer = new TransactionMQProducer("please_rename_unique_group_name"); //设置生产者的本地事物回调组件 producer.setTransactionListener(transactionListener); producer.start(); //发送消息 SendResult sendResult = producer.sendMessageInTransaction(msg, null); producer.shutdown(); } }
回调组件TransactionListenerImpl代码如下:
public class TransactionListenerImpl implements TransactionListener {
//本地事务业务逻辑
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
//这里会执行本地业务逻辑,此处省略...
//返回本地事物的执行结果(UNKNOW、commit、rollback)
return LocalTransactionState.UNKNOW;
}
//当prepare发送后超时没有返回,那么MQ服务器会回调执行这个方法用来检查上次本地事务执行的状态,
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
//这里会实现检查本地事物处理结果的逻辑,此处省略...
//比如如果本地事物是往表A插入一条数据的话,那么此处可以去表A查下那条记录是否存在,就可以知道上次本地事物是否成功了
//根据上次本地事物执行的结果返回消息状态
//本地事物执行成功返回COMMIT_MESSAGE,反之失败返回ROLLBACK_MESSAGE
return LocalTransactionState.COMMIT_MESSAGE;
}
}
以上是官网例子中我精简过的代码,关键的一点是事务消息的生产者需要构造实现了TransactionListener的实现类,并注册到TransactionMQProducer中,接下来我们就以此为入口按照上面的事物流程图来分析下源码吧
首先肯定是从生产中发送消息开始喽
//发送消息 SendResult sendResult = producer.sendMessageInTransaction(msg, null);
最终会调用到org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#sendMessageInTransaction中来
这个方法非常重要,实现了事物消息发送的关键逻辑(发送消息->执行本地事物->commit/rollback消息)
public class DefaultMQProducerImpl implements MQProducerInner { public TransactionSendResult sendMessageInTransaction(final Message msg, final LocalTransactionExecuter localTransactionExecuter, final Object arg) throws MQClientException { //获取之前注册的transactionListener本地事务回查组件 TransactionListener transactionListener = getCheckListener(); try { //发送prepare消息 sendResult = this.send(msg); } catch (Exception e) { throw new MQClientException("send message Exception", e); } LocalTransactionState localTransactionState = LocalTransactionState.UNKNOW; Throwable localException = null; switch (sendResult.getSendStatus()) { case SEND_OK: {//发送prepare消息成功 try { if (sendResult.getTransactionId() != null) { msg.putUserProperty("__transactionId__", sendResult.getTransactionId()); } String transactionId = msg.getProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX); if (null != transactionId && !"".equals(transactionId)) { msg.setTransactionId(transactionId); } //localTransactionExecuter传进来为null,这个分支不会走 if (null != localTransactionExecuter) { localTransactionState = localTransactionExecuter.executeLocalTransactionBranch(msg, arg); } else if (transactionListener != null) { log.debug("Used new transaction API"); //执行本地事务 localTransactionState = transactionListener.executeLocalTransaction(msg, arg); } if (null == localTransactionState) { localTransactionState = LocalTransactionState.UNKNOW; } if (localTransactionState != LocalTransactionState.COMMIT_MESSAGE) { log.info("executeLocalTransactionBranch return {}", localTransactionState); log.info(msg.toString()); } } catch (Throwable e) { log.info("executeLocalTransactionBranch exception", e); log.info(msg.toString()); localException = e; } } break; case FLUSH_DISK_TIMEOUT: case FLUSH_SLAVE_TIMEOUT: case SLAVE_NOT_AVAILABLE: localTransactionState = LocalTransactionState.ROLLBACK_MESSAGE; break; default: break; } try { //根据本地事务执行的结果去发送commit消息或者rollback消息 this.endTransaction(sendResult, localTransactionState, localException); } catch (Exception e) { log.warn("local transaction execute " + localTransactionState + ", but end broker transaction failed", e); } TransactionSendResult transactionSendResult = new TransactionSendResult(); transactionSendResult.setSendStatus(sendResult.getSendStatus()); transactionSendResult.setMessageQueue(sendResult.getMessageQueue()); transactionSendResult.setMsgId(sendResult.getMsgId()); transactionSendResult.setQueueOffset(sendResult.getQueueOffset()); transactionSendResult.setTransactionId(sendResult.getTransactionId()); transactionSendResult.setLocalTransactionState(localTransactionState); return transactionSendResult; }}
我们先看下sendResult = this.send(msg);这行代码内部是怎么执行的,其实它最终调用的是MQClientAPIImpl组件的sendMessage()方法,代码调用顺序如下:
public class DefaultMQProducerImpl implements MQProducerInner { /** * DEFAULT SYNC ------------------------------------------------------- */ public SendResult send( Message msg) throws MQClientException, RemotingException, MQBrokerException, InterruptedException { return send(msg, this.defaultMQProducer.getSendMsgTimeout()); } public SendResult send(Message msg, long timeout) throws MQClientException, RemotingException, MQBrokerException, InterruptedException { return this.sendDefaultImpl(msg, CommunicationMode.SYNC, null, timeout); } private SendResult sendDefaultImpl( Message msg, final CommunicationMode communicationMode, final SendCallback sendCallback, final long timeout ) throws MQClientException, RemotingException, MQBrokerException, InterruptedException { this.makeSureStateOK(); Validators.checkMessage(msg, this.defaultMQProducer); sendResult = this.sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout - costTime); } private SendResult sendKernelImpl(final Message msg, final MessageQueue mq, final CommunicationMode communicationMode, final SendCallback sendCallback, final TopicPublishInfo topicPublishInfo, final long timeout) throws MQClientException, RemotingException, MQBrokerException, InterruptedException { SendResult sendResult = null; switch (communicationMode) { case ASYNC: //省略非关键代码 break; case ONEWAY: case SYNC://communicationMode传参时为SYNC,默认走的这个分支 long costTimeSync = System.currentTimeMillis() - beginStartTime; if (timeout < costTimeSync) { throw new RemotingTooMuchRequestException("sendKernelImpl call timeout"); } //发送消息 sendResult = this.mQClientFactory.getMQClientAPIImpl().sendMessage( brokerAddr, mq.getBrokerName(), msg, requestHeader, timeout - costTimeSync, communicationMode, context, this); break; default: assert false; break; } }}
我们接着MQClientAPIImpl组件的sendMessage()方法继续往下看
public class MQClientAPIImpl { public SendResult sendMessage( final String addr, final String brokerName, final Message msg, final SendMessageRequestHeader requestHeader, final long timeoutMillis, final CommunicationMode communicationMode, final SendMessageContext context, final DefaultMQProducerImpl producer ) throws RemotingException, MQBrokerException, InterruptedException { return sendMessage(addr, brokerName, msg, requestHeader, timeoutMillis, communicationMode, null, null, null, 0, context, producer); } public SendResult sendMessage( final String addr, final String brokerName, final Message msg, final SendMessageRequestHeader requestHeader, final long timeoutMillis, final CommunicationMode communicationMode, final SendCallback sendCallback, final TopicPublishInfo topicPublishInfo, final MQClientInstance instance, final int retryTimesWhenSendFailed, final SendMessageContext context, final DefaultMQProducerImpl producer ) throws RemotingException, MQBrokerException, InterruptedException { long beginStartTime = System.currentTimeMillis(); RemotingCommand request = null; if (sendSmartMsg || msg instanceof MessageBatch) { SendMessageRequestHeaderV2 requestHeaderV2 = SendMessageRequestHeaderV2.createSendMessageRequestHeaderV2(requestHeader); request = RemotingCommand.createRequestCommand(msg instanceof MessageBatch ? RequestCode.SEND_BATCH_MESSAGE : RequestCode.SEND_MESSAGE_V2, requestHeaderV2); } else { request = RemotingCommand.createRequestCommand(RequestCode.SEND_MESSAGE, requestHeader); } request.setBody(msg.getBody()); switch (communicationMode) { case ONEWAY: this.remotingClient.invokeOneway(addr, request, timeoutMillis); return null; case ASYNC: final AtomicInteger times = new AtomicInteger(); long costTimeAsync = System.currentTimeMillis() - beginStartTime; if (timeoutMillis < costTimeAsync) { throw new RemotingTooMuchRequestException("sendMessage call timeout"); } this.sendMessageAsync(addr, brokerName, msg, timeoutMillis - costTimeAsync, request, sendCallback, topicPublishInfo, instance, retryTimesWhenSendFailed, times, context, producer); return null; case SYNC://communicationMode传参时为SYNC,默认走的这个代码分支 long costTimeSync = System.currentTimeMillis() - beginStartTime; if (timeoutMillis < costTimeSync) { throw new RemotingTooMuchRequestException("sendMessage call timeout"); } //以同步的方式发送消息 return this.sendMessageSync(addr, brokerName, msg, timeoutMillis - costTimeSync, request); default: assert false; break; } return null; } private SendResult sendMessageSync( final String addr, final String brokerName, final Message msg, final long timeoutMillis, final RemotingCommand request ) throws RemotingException, MQBrokerException, InterruptedException { //后边就是调用netty相关组件来处理发送msg了 RemotingCommand response = this.remotingClient.invokeSync(addr, request, timeoutMillis); assert response != null; return this.processSendResponse(brokerName, msg, response); }}
由以上代码可知最终会调用到NettyRemotingClient组件的invokeSync()方法进行处理,在这个invokeSync()方法中会通过nio的方式把消息发送到MQ服务器端broker,invokeSync()方法的源码如下
public class NettyRemotingClient extends NettyRemotingAbstract implements RemotingClient { @Override public RemotingCommand invokeSync(String addr, final RemotingCommand request, long timeoutMillis) throws InterruptedException, RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException { long beginStartTime = System.currentTimeMillis(); final Channel channel = this.getAndCreateChannel(addr); if (channel != null && channel.isActive()) { try { doBeforeRpcHooks(addr, request); long costTime = System.currentTimeMillis() - beginStartTime; if (timeoutMillis < costTime) { throw new RemotingTimeoutException("invokeSync call timeout"); } RemotingCommand response = this.invokeSyncImpl(channel, request, timeoutMillis - costTime); doAfterRpcHooks(RemotingHelper.parseChannelRemoteAddr(channel), request, response); return response; } catch (RemotingSendRequestException e) { log.warn("invokeSync: send request exception, so close the channel[{}]", addr); this.closeChannel(addr, channel); throw e; } catch (RemotingTimeoutException e) { if (nettyClientConfig.isClientCloseSocketIfTimeout()) { this.closeChannel(addr, channel); log.warn("invokeSync: close socket because of timeout, {}ms, {}", timeoutMillis, addr); } log.warn("invokeSync: wait response timeout exception, the channel[{}]", addr); throw e; } } else { this.closeChannel(addr, channel); throw new RemotingConnectException(addr); } }}
MQ服务器端broker处理完发送过来的消息之后会给生产者DefaultMQProducerImpl一个返回值SendResult。
到这里为止我们执行到事物消息的哪个阶段了呢?
现在我们回到关键流程中去,关键流程就在DefaultMQProducerImpl组件的sendMessageInTransaction()方法中
然后我们发现其实我们刚把这段代码sendResult = this.send(msg)执行完毕并拿到了一个sendResult返回值,这个返回值代表着我们发送这条消息的一个结果(发送成功还是失败)
public class DefaultMQProducerImpl implements MQProducerInner { public TransactionSendResult sendMessageInTransaction(final Message msg, final LocalTransactionExecuter localTransactionExecuter, final Object arg) throws MQClientException { //省略无关代码... //发送prepare消息 sendResult = this.send(msg);}
接下来会根据sendResult返回值来执行不同的逻辑处理
如果sendResult为SEND_OK,即发送prepare消息成功,那么就开始执行本地事物(即TransactionListenerImpl组件的executeLocalTransaction()方法)
本地事物返回值是一个枚举localTransactionState(有3种取值 COMMIT_MESSAGE , ROLLBACK_MESSAGE , UNKNOW)
如果发送prepare消息失败,则直接设置localTransactionState的值为ROLLBACK_MESSAGE。最后执行endTransaction()方法进行prepare消息的二次确认。源码如下
public class DefaultMQProducerImpl implements MQProducerInner { public TransactionSendResult sendMessageInTransaction(final Message msg, final LocalTransactionExecuter localTransactionExecuter, final Object arg) throws MQClientException { //获取之前注册的transactionListener本地事务回查组件 TransactionListener transactionListener = getCheckListener(); try { //发送prepare消息 sendResult = this.send(msg); } catch (Exception e) { throw new MQClientException("send message Exception", e); } LocalTransactionState localTransactionState = LocalTransactionState.UNKNOW; Throwable localException = null; switch (sendResult.getSendStatus()) { case SEND_OK: {//发送prepare消息成功 try { if (sendResult.getTransactionId() != null) { msg.putUserProperty("__transactionId__", sendResult.getTransactionId()); } String transactionId = msg.getProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX); if (null != transactionId && !"".equals(transactionId)) { msg.setTransactionId(transactionId); } //localTransactionExecuter传进来为null,这个分支不会走 if (null != localTransactionExecuter) { localTransactionState = localTransactionExecuter.executeLocalTransactionBranch(msg, arg); } else if (transactionListener != null) { log.debug("Used new transaction API"); //执行本地事务 localTransactionState = transactionListener.executeLocalTransaction(msg, arg); } } catch (Throwable e) { log.info("executeLocalTransactionBranch exception", e); log.info(msg.toString()); localException = e; } } break; case FLUSH_DISK_TIMEOUT: case FLUSH_SLAVE_TIMEOUT: case SLAVE_NOT_AVAILABLE: localTransactionState = LocalTransactionState.ROLLBACK_MESSAGE; break; default: break; } try { //根据本地事务执行的结果去发送commit消息或者rollback消息 this.endTransaction(sendResult, localTransactionState, localException); } catch (Exception e) { log.warn("local transaction execute " + localTransactionState + ", but end broker transaction failed", e); } }}
我们继续接着endTransaction()往下看
public class DefaultMQProducerImpl implements MQProducerInner { public void endTransaction( EndTransactionRequestHeader requestHeader = new EndTransactionRequestHeader(); requestHeader.setTransactionId(transactionId); requestHeader.setCommitLogOffset(id.getOffset()); //根据本地事务执行的结果去设置请求头信息commitOrRollback switch (localTransactionState) { case COMMIT_MESSAGE: requestHeader.setCommitOrRollback(MessageSysFlag.TRANSACTION_COMMIT_TYPE); break; case ROLLBACK_MESSAGE: requestHeader.setCommitOrRollback(MessageSysFlag.TRANSACTION_ROLLBACK_TYPE); break; case UNKNOW: requestHeader.setCommitOrRollback(MessageSysFlag.TRANSACTION_NOT_TYPE); break; default: break; } requestHeader.setProducerGroup(this.defaultMQProducer.getProducerGroup()); requestHeader.setTranStateTableOffset(sendResult.getQueueOffset()); requestHeader.setMsgId(sendResult.getMsgId()); String remark = localException != null ? ("executeLocalTransactionBranch exception: " + localException.toString()) : null; //将request写入channel中,通过socket把消息发送给消费端,后边主要是netty的事儿了 this.mQClientFactory.getMQClientAPIImpl().endTransactionOneway(brokerAddr, requestHeader, remark, this.defaultMQProducer.getSendMsgTimeout()); }}
public class MQClientAPIImpl { public void endTransactionOneway( final String addr, final EndTransactionRequestHeader requestHeader, final String remark, final long timeoutMillis ) throws RemotingException, MQBrokerException, InterruptedException { RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.END_TRANSACTION, requestHeader); request.setRemark(remark); //将request写入channel中,通过socket把消息发送给消费端,后边主要是netty的事儿了 this.remotingClient.invokeOneway(addr, request, timeoutMillis); }}
public class NettyRemotingClient extends NettyRemotingAbstract implements RemotingClient { @Override public void invokeOneway(String addr, RemotingCommand request, long timeoutMillis) throws InterruptedException, RemotingConnectException, RemotingTooMuchRequestException, RemotingTimeoutException, RemotingSendRequestException { final Channel channel = this.getAndCreateChannel(addr); if (channel != null && channel.isActive()) { try { doBeforeRpcHooks(addr, request); this.invokeOnewayImpl(channel, request, timeoutMillis); } catch (RemotingSendRequestException e) { log.warn("invokeOneway: send request exception, so close the channel[{}]", addr); this.closeChannel(addr, channel); throw e; } } else { this.closeChannel(addr, channel); throw new RemotingConnectException(addr); } }}
最终调用NettyRemotingClient组件的invokeOneway()方法完成prepare消息的二次确认,如果localTransactionState的值为COMMIT_MESSAGE时则MQ服务端会将消息投递给消费者进行消费;
但是如果localTransactionState的值为ROLLBACK_MESSAGE时则MQ服务端会删除已经存储的prepare消息,此时消费者将没有机会消费到这条消息。
二、Broker端对消息的处理
(1)Broker处理prepare消息
最终调用SendMessageProcessor组件的sendMessage
方法处理事务消息,代码如下
public class SendMessageProcessor extends AbstractSendMessageProcessor implements NettyRequestProcessor { private RemotingCommand sendMessage(final ChannelHandlerContext ctx, final RemotingCommand request, final SendMessageContext sendMessageContext, final SendMessageRequestHeader requestHeader) throws RemotingCommandException { //判断是否是事务消息 如果是事务消息则用事务消息的逻辑处理 String traFlag = oriProps.get(MessageConst.PROPERTY_TRANSACTION_PREPARED); if (traFlag != null && Boolean.parseBoolean(traFlag)) { if (this.brokerController.getBrokerConfig().isRejectTransactionMessage()) { response.setCode(ResponseCode.NO_PERMISSION); response.setRemark( "the broker[" + this.brokerController.getBrokerConfig().getBrokerIP1() + "] sending transaction message is forbidden"); return response; } //处理prepare消息 putMessageResult = this.brokerController.getTransactionalMessageService().prepareMessage(msgInner); } else { putMessageResult = this.brokerController.getMessageStore().putMessage(msgInner); } }}
public class TransactionalMessageServiceImpl implements TransactionalMessageService { @Override public PutMessageResult prepareMessage(MessageExtBrokerInner messageInner) { return transactionalMessageBridge.putHalfMessage(messageInner); }}
public class TransactionalMessageBridge { public PutMessageResult putHalfMessage(MessageExtBrokerInner messageInner) { //对prepare消息进行存储 return store.putMessage(parseHalfMessageInner(messageInner)); }}
(2)Broker处理prepare消息的二次确认,即结束事务消息的处理
2.1 会判断本次事务的最终状态,如果是Commit就改变事物消息状态,使消费者可见,此时消费者就可以消费消息了
2.2 如果是Rollback,那么就删除在broker端存储的事物消息,此时消费者就永远消费不到这条消息
大家可以先这样理解,因为这块比较复杂,实际是通过3个队列实现的,之后有机会我会详细说下这块的实现细节。
三、事务消息是如何处理回查的?
broker在启动时会启动线程回查的服务,在TransactionMessageCheckService
的run
方法中,该方法会执行到onWaitEnd方法:
@Overrideprotected void onWaitEnd() { //获取超时时间 6s long timeout = brokerController.getBrokerConfig().getTransactionTimeOut(); //获取最大检测次数 15次 int checkMax = brokerController.getBrokerConfig().getTransactionCheckMax(); //获取当前时间 long begin = System.currentTimeMillis(); log.info("Begin to check prepare message, begin time:{}", begin); //开始检测 this.brokerController.getTransactionalMessageService().check(timeout, checkMax, this.brokerController.getTransactionalMessageCheckListener()); log.info("End to check prepare message, consumed time:{}", System.currentTimeMillis() - begin);}
通过netty传递消息最终调用到TransactionListenerImpl组件的checkLocalTransaction()方法来检查本地事物的状态
public class TransactionListenerImpl implements TransactionListener {
//当prepare发送后超时没有返回,那么MQ服务器会回调执行这个方法用来检查上次本地事务执行的状态,
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
//这里会实现检查本地事物处理结果的逻辑,此处省略...
//比如如果本地事物是往表A插入一条数据的话,那么此处可以去表A查下那条记录是否存在,就可以知道上次本地事物是否成功了
//根据上次本地事物执行的结果返回消息状态
//本地事物执行成功返回COMMIT_MESSAGE,反之失败返回ROLLBACK_MESSAGE
return LocalTransactionState.COMMIT_MESSAGE;
}
}
四、总结
本篇文章从生产者发送prepare消息、Broker端对消息的处理以及事务消息是如何处理回查的3个阶段详细分析了rocketMQ的源码,按照上述代码执行顺序大家完全可以跟着走读下源码。
为了给大家展示到事物消息的核心链路,很多细节我都隐藏掉了,大家可以在走读源码的时候自己研究下。
五、关于rocketMQ某些版本不支持事务消息回查的说明
RocketMQ 3.0.8 以及之前的版本是 支持分布式事务消息;
RocketMQ 3.0.8 之后 ,分布式事务的阉割了,不支持分布式事务消息;
RocketMQ 4.0.0 开始 apache 孵化,但是也不支持分布式事务消息;
2018年08月, RocketMQ 4.3.0 又开始支持分布式事务消息。
http://rocketmq.apache.org/release_notes/release-notes-4.3.0/ ,如图所示:
总结:
从RocketMQ 3.0.8 之后 到 4.3.0 之前,在这期间的版本均不支持分布式事务消息。包括这在期间使用比较广泛的3.2.6 就是不支持分布式事务消息。
注意上边说的不支持仅仅是指源码中缺少了broker事务回查的代码,其他的事物代码还是存在的,我是对比了4.2.0和4.3.0的源码发现的,大家有兴趣也可以关注下这块。
END
石杉的架构笔记(id:shishan100)
作者:中华石杉,多年BAT架构经验倾囊相授
热门评论
这么乱的排版,作者都要看不下去了,抄也抄的太不走心了