Apache RocketMQ Producer解析

基于RocketMQ源代码版本:rocketmq-all-4.5.2-source-release

1、RocketMQ生产者-核心参数

参数名 默认值 说明
producerGroup DEFAULT_PRODUCER Producer组名,多个Producer如果属于一个应用,发送同样的消息,则应该将它们归为同一组。
createTopicKey TBW102 在发送消息时,自动创建服务器不存在的topic,需要指定key
defaultTopicQueueNums 4 在发送消息时,自动创建服务器不存在的topic,默认创建的队列数
sendMsgTimeout 10000 发送消息超时时间,单位毫秒
compressMsgBodyOverHowmuch 4096 消息Body超过多大开始压缩(Consumer收到消息会自动解压缩),单位字节
retryAnotherBrokerWhenNotStoreOK FALSE 如果发送消息返回sendResult,但是sendStatus!=SEND_OK,是否重试发送
maxMessageSize 131072 客户端限制的消息大小,超过报错,同时服务端也会限制(默认128K)
transactionCheckListener 事物消息回查监听器,如果发送事务消息,必须设置
checkThreadPoolMinSize 1 Broker回查Producer事务状态时,线程池大小
checkThreadPoolMaxSize 1 Broker回查Producer事务状态时,线程池大小
checkRequestHoldMax 2000 Broker回查Producer事务状态时,Producer本地缓冲请求队列大小

2、RocketMQ主从同步机制解析

Master-Slave主从同步
同步信息:消息数据内容(commitLog) + 元数据信息(topic配置信息、消费者偏移量Offset、延迟偏移量Offset等配置信息)

元数据同步:Broker角色识别,为Slave则启动同步定时任务,由Netty实现

  • 在BrokerController的handleSlaveSynchronize()方法中
/**
 * 该方法的主要作用是处理从节点的元数据同步,
 * 即从节点向主节点主动同步 topic 的路由信息、消费进度、延迟队列处理队列、消费组订阅配置等信息。
 * @param role
 */
private void handleSlaveSynchronize(BrokerRole role) {
    //如果角色为Slave
    if (role == BrokerRole.SLAVE) {
        if (null != slaveSyncFuture) {
            //如果上次同步的 future 不为空,则首先先取消
            slaveSyncFuture.cancel(false);
        }
        //然后设置 slaveSynchronize 的 master 地址为空
        this.slaveSynchronize.setMasterAddr(null);
        
        //在固定时间开启只有一个线程的定时任务,每 10s 从主节点同步一次配置数据
        slaveSyncFuture = this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                try {
                    //启动定时任务进行元数据信息的同步
                    BrokerController.this.slaveSynchronize.syncAll();
                }
                catch (Throwable e) {
                    log.error("ScheduledTask SlaveSynchronize syncAll error.", e);
                }
            }
        }, 1000 * 3, 1000 * 10, TimeUnit.MILLISECONDS);
    } else {
        //handle the slave synchronise
        //如果当前节点的角色为主节点,则取消定时同步任务并设置 master 的地址为空
        if (null != slaveSyncFuture) {
            slaveSyncFuture.cancel(false);
        }
        this.slaveSynchronize.setMasterAddr(null);
    }
}

public void changeToSlave(int brokerId) {
    ......
    //handle the slave synchronise
         //操作主从同步
    handleSlaveSynchronize(BrokerRole.SLAVE);
    ......
}
  • 元数据同步内容:SlaveSynchronize
public void syncAll() {
    this.syncTopicConfig(); //同步topic配置信息
    this.syncConsumerOffset();  //同步消费者偏移量Offset
    this.syncDelayOffset(); //同步延迟偏移量Offset
    this.syncSubscriptionGroupConfig(); //订阅组配置信息
}

消息数据内容(commitLog)同步:时实同步由java原生Socket实现,HAService、HAconnection、WaitNotifyObject

HAService:主从同步核心实现类。
Master节点:

  • 1、AcceptSocketService acceptSocketService:服务端接收连接线程实现类,作为Master端监听Slave连接的实现类。
  • 2、HAConnection:HA Master-Slave 网络连接对象,对Master节点连接、读写数据。
  • A、WriteSocketService writeSocketService:HAConnection网络写封装,写到Slave节点的数据。
  • B、ReadSocketService readSocketService:HAConnection网络读封装,读取来自Slave节点的数据。

Slave节点:

  • HAClient haClient:HA客户端实现,Slave端网络的实现类。

RocketMQ主从同步基本实现过程如下图所示:


image.png

RocketMQ 的主从同步机制如下:

  • 1、首先启动Master并在指定端口监听Slave的连接请求;
  • 2、Slave启动,主动连接Master,建立TCP连接;
  • 3、Slave以每隔5s的间隔时间向Master拉取消息,如果是第一次拉取的话,先获取本地commitlog文件中最大的偏移量Offset,以该偏移量Offset向Master拉取消息;
  • 4、Master解析请求,并返回一批数据给Slave;
  • 5、Slave收到一批消息后,将消息写入本地commitlog文件中,然后向Master汇报拉取进度,并更新下一次待拉取偏移量Offset;
  • 6、然后重复第3步;

通信协议:Master节点与Slave节点通信协议很简单,如下:

  • Slave ====> Master 上报CommitLog已经同步到的物理位置
  • Master ====> 传输新的CommitLog数据

源码研究RocketMQ主从同步机制(HA)https://blog.csdn.net/prestigeding/article/details/79600792

RocketMQ主从同步一个重要的特征:

  • RocketMQ4.5.0版本之前,主从同步不具备主从切换功能,即当主节点宕机后,从不会接管消息发送,但可以提供消息读取。
  • RocketMQ4.5.0版本之后,rocketmq基于raft 协议支持主从切换,引入了多副本机制,即 DLedger,支持主从切换,即当一个复制组内的主节点宕机后,会在该复制组内触发重新选主,选主完成后即可继续提供消息写功能。rocketmq主从切换基于 raft 协议,而Zookeeper也是基于该协议。

关于Raft协议请点击:一文搞懂Raft算法

主从切换的主要逻辑在BrokerController类中,具体如下:

Broker 角色变更为Slave

/**
 * Broker 角色变更为Slave
 * @param brokerId
 */
public void changeToSlave(int brokerId) {
    log.info("Begin to change to slave brokerName={} brokerId={}", brokerConfig.getBrokerName(), brokerId);

    //change the role
    //设置 brokerId,如果broker的id为0,则设置为1,这里在使用的时候,注意规划好集群内节点的 brokerId
    brokerConfig.setBrokerId(brokerId == 0 ? 1 : brokerId); //TO DO check
    //设置 broker  的角色为 BrokerRole.SLAVE。
    messageStoreConfig.setBrokerRole(BrokerRole.SLAVE);

    //handle the scheduled service
    try {
        //从节点,则关闭定时调度线程(处理 RocketMQ 延迟队列),如果是主节点,则启动该线程。
        this.messageStore.handleScheduleMessageService(BrokerRole.SLAVE);
    } catch (Throwable t) {
        log.error("[MONITOR] handleScheduleMessageService failed when changing to slave", t);
    }

    //handle the transactional service
    try {
        //关闭事务状态回查处理器
        this.shutdownProcessorByHa();
    } catch (Throwable t) {
        log.error("[MONITOR] shutdownProcessorByHa failed when changing to slave", t);
    }

    //handle the slave synchronise
    //从节点需要启动配置信息同步处理器,即启动 SlaveSynchronize 定时从主服务器同步元数据等配置信息
    handleSlaveSynchronize(BrokerRole.SLAVE);

    try {
        //立即向集群内所有的 nameserver 告知 broker  信息状态的变更
        this.registerBrokerAll(true, true, brokerConfig.isForceRegister());
    } catch (Throwable ignored) {

    }
    log.info("Finish to change to slave brokerName={} brokerId={}", brokerConfig.getBrokerName(), brokerId);
}

/**
 * 关闭事务状态回查处理器,当Master变更Slave后,该方法被调用。
 */
private void shutdownProcessorByHa() {
    if (this.transactionalMessageCheckService != null) {
        this.transactionalMessageCheckService.shutdown(true);
    }
}

Broker 角色从Slave变更为Master

/**
 * Broker 角色从Slave变更为Master的处理逻辑
 * @param role
 */
public void changeToMaster(BrokerRole role) {
    if (role == BrokerRole.SLAVE) {
        return;
    }
    log.info("Begin to change to master brokerName={}", brokerConfig.getBrokerName());

    //handle the slave synchronise
    //Master节点,会在该方法中设置slaveSyncFuture.cancel(false);
    handleSlaveSynchronize(role);

    //handle the scheduled service
    //开启定时任务处理线程。
    try {
        this.messageStore.handleScheduleMessageService(role);
    } catch (Throwable t) {
        log.error("[MONITOR] handleScheduleMessageService failed when changing to master", t);
    }

    //handle the transactional service
    //开启事务状态回查处理线程。
    try {
        this.startProcessorByHa(BrokerRole.SYNC_MASTER);
    } catch (Throwable t) {
        log.error("[MONITOR] startProcessorByHa failed when changing to master", t);
    }

    //if the operations above are totally successful, we change to master
    //设置 brokerId 为 0,配置文件中brokerId为0是Master节点
    brokerConfig.setBrokerId(0); //TO DO check
    messageStoreConfig.setBrokerRole(role);

    try {
        //向 nameserver 立即发送心跳包以便告知 broker 服务器当前最新的状态
        this.registerBrokerAll(true, true, brokerConfig.isForceRegister());
    } catch (Throwable ignored) {

    }
    log.info("Finish to change to master brokerName={}", brokerConfig.getBrokerName());
}

/**
 * 该方法的作用是开启事务状态回查处理器,
 * 即当节点为Master时,开启对应的事务状态回查处理器,对PREPARE状态的消息发起事务状态回查请求
 * @param role
 */
private void startProcessorByHa(BrokerRole role) {
    if (BrokerRole.SLAVE != role) {
        if (this.transactionalMessageCheckService != null) {
            this.transactionalMessageCheckService.start();
        }
    }
}

RocketMQ 主从切换DLedger 是基于raft协议实现的,在该协议中就实现了主节点的选举与主节点失效后集群会自动进行重新选举,经过协商投票产生新的主节点,从而实现高可用。
BrokerController#initialize(),在 Broker 启动时,如果开启了多副本机制,即 enableDLedgerCommitLog 参数设置为 true,会为 集群节点选主器添加 roleChangeHandler 事件处理器,即节点发送变更后的事件处理器。

if (result) {
    try {
        this.messageStore =
            new DefaultMessageStore(this.messageStoreConfig, this.brokerStatsManager, this.messageArrivingListener,
                this.brokerConfig);
        if (messageStoreConfig.isEnableDLegerCommitLog()) {
            DLedgerRoleChangeHandler roleChangeHandler = new DLedgerRoleChangeHandler(this, (DefaultMessageStore) messageStore);
            ((DLedgerCommitLog)((DefaultMessageStore) messageStore).getCommitLog()).getdLedgerServer().getdLedgerLeaderElector().addRoleChangeHandler(roleChangeHandler);
        }
        this.brokerStats = new BrokerStats((DefaultMessageStore) this.messageStore);
        //load plugin
        MessageStorePluginContext context = new MessageStorePluginContext(messageStoreConfig, brokerStatsManager, messageArrivingListener, brokerConfig);
        this.messageStore = MessageStoreFactory.build(context, this.messageStore);
        this.messageStore.getDispatcherList().addFirst(new CommitLogDispatcherCalcBitMap(this.brokerConfig, this.consumerFilterManager));
    } catch (IOException e) {
        result = false;
        log.error("Failed to initialize", e);
    }
}

DLedgerRoleChangeHandler#handle(),主从状态切换的逻辑

/**
 * handle 主从状态切换处理逻辑
 * @param term
 * @param role
 */
@Override 
public void handle(long term, MemberState.Role role) {
    Runnable runnable = new Runnable() {
        @Override public void run() {
            long start = System.currentTimeMillis();
            try {
                boolean succ = true;
                log.info("Begin handling broker role change term={} role={} currStoreRole={}", term, role, messageStore.getMessageStoreConfig().getBrokerRole());
                switch (role) {
                    case CANDIDATE:
                        //如果当前节点状态机状态为 CANDIDATE,表示正在发起 Leader 节点,如果该服务器的角色不是 SLAVE 的话,需要将状态切换为 SLAVE
                        if (messageStore.getMessageStoreConfig().getBrokerRole() != BrokerRole.SLAVE) {
                            brokerController.changeToSlave(dLedgerCommitLog.getId());
                        }
                        break;
                    case FOLLOWER:
                        //如果当前节点状态机状态为 FOLLOWER,broker 节点将转换为 从节点
                        brokerController.changeToSlave(dLedgerCommitLog.getId());
                        break;
                    case LEADER:
                        //如果当前节点状态机状态为 Leader,说明该节点被选举为 Leader,
                        //在切换到 Master 节点之前,首先需要等待当前节点追加的数据都已经被提交后才可以将状态变更为 Master
                        while (true) {
                            if (!dLegerServer.getMemberState().isLeader()) {
                                succ = false;
                                break;
                            }
                            if (dLegerServer.getdLedgerStore().getLedgerEndIndex() == -1) {
                                //如果 ledgerEndIndex 为 -1,表示当前节点还没有数据转发,直接跳出循环,无需等待
                                break;
                            }
                            if (dLegerServer.getdLedgerStore().getLedgerEndIndex() == dLegerServer.getdLedgerStore().getCommittedIndex()
                                && messageStore.dispatchBehindBytes() == 0) {
                                //如果 ledgerEndIndex 不为 -1 ,则必须等待数据都已提交,即 ledgerEndIndex 与 committedIndex 相等
                                break;
                            }
                            Thread.sleep(100);
                        }
                        if (succ) {
                            //并且需要等待  commitlog 日志全部已转发到 consumequeue中,
                            // 即 ReputMessageService 中的 reputFromOffset 与 commitlog 的 maxOffset 相等
                            messageStore.recoverTopicQueueTable();

                            //等待上述条件满足后,即可以进行状态的变更,需要恢复 ConsumeQueue,
                            // 维护每一个 queue 对应的 maxOffset,然后将 broker 角色转变为 master
                            brokerController.changeToMaster(BrokerRole.SYNC_MASTER);
                        }
                        break;
                    default:
                        break;
                }
                log.info("Finish handling broker role change succ={} term={} role={} currStoreRole={} cost={}", succ, term, role, messageStore.getMessageStoreConfig().getBrokerRole(), DLedgerUtils.elapsed(start));
            } catch (Throwable t) {
                log.info("[MONITOR]Failed handling broker role change term={} role={} currStoreRole={} cost={}", term, role, messageStore.getMessageStoreConfig().getBrokerRole(), DLedgerUtils.elapsed(start), t);
            }
        }
    };
    executorService.submit(runnable);
}

3、RocketMQ同步消息发送

消息的同步发送:producer.send(msg)
同步发送消息核心实现:DefaultMQProducerImpl

4、RocketMQ异步消息发送

消息的异步发送:producer.send(Message msg, SendCallBack sendCallBack)
异步发送消息核心实现:DefaultMQProducerImpl

5、Netty底层通信框架解析

rocketmq底层网络使用的netty框架,类图如下


image.png

Remoting模块类结构图:


image.png
  • RecketMQ通信模块的顶层结构是RemotingServer和RemotingClient,分别对应通信的服务端和客户端

  • RemotingServer类中比较重要的是:localListenPort、registerProcessor和registerDefaultProcessor,registerDefaultProcesor用来设置接收到消息后的处理方法。

  • RemotingClient类和RemotingServer类相对应,比较重要的方法是updateNameServerAddressList、invokeSync和invokeOneway、updateNameServerAddresList用来获取有效的NameServer地址,invokeSync与invokeOneway用来向Server端发送请求

  • NettyRemotingServer和NettyRemotingClient分别实现了RemotingServer和RemotingClient这两个接口,但它们有很多共有的内容,比如invokeSync、invokeOneway等,所以这些共有函数被提取到NettyRemotingAbstract共同继承的父类中。

  • 无论是服务端还是客户端都需要处理接收到的请求,处理方法由processMessageReceived定义,注意这里接收到的消息已经被转换成RemotingCommand了,而不是原始的字节流。

  • RemotingCommand是RocketMQ自定义的协议,具体格式如下


    image
  • 这个协议只有四部分,但是覆盖了RocketMQ各个角色间几乎所有的通信过程,RemotingCommand有实际的数据类型和各部分对应,如下所示。

private int code;
private LanguageCode language = LanguageCode.JAVA;
private int version = 0;
private int opaque = requestId.getAndIncrement();
private int flag = 0;
private String remark;
private HashMap<String, String> extFields;
private transient CommandCustomHeader customHeader;

private SerializeType serializeTypeCurrentRPC = serializeTypeConfigInThisServer;

private transient byte[] body;
  • RocketMQ各个组件间的通信需要频繁地在字节码和RemotingCommand间相互转换,也就是编码、解码过程,好在Netty提供了codec支持,这个频繁地操作只需要一行设置即可:pipeline().addLoast(newNettyEncoder(), now NettyDecoder() )

  • RocketMQ对通信过程的另一个抽象是Processor和Executor,当接收到一个消息后,直接根据消息的类型调用对应的Processor和Executor,把通信过程和业务逻辑分离开来。

  • 具体的rocketmq netty底层设计源码:rocketmq netty底层设计

6、RocketMQ生产者-消息返回状态详解

public enum SendStatus {
    SEND_OK,
    FLUSH_DISK_TIMEOUT,
    FLUSH_SLAVE_TIMEOUT,
    SLAVE_NOT_AVAILABLE,
}
  • 1、SEND_OK:消息发送成功
  • 2、FLUSH_DISK_TIMEOUT:消息发送成功,但服务在进行刷盘的时候超时了
    消息已经进入服务器队列,刷盘超时会等待下一次的刷盘时机再次刷盘,如果此时服务器down机消息丢失,会返回此种状态,如果业务系统是可靠性消息投递,那么需要重发消息。
  • 3、FLUSH_SLAVE_TIMEOUT:在主从同步的时候,同步到Slave超时了
    如果此时Master节点down机,消息也会丢失
  • 4、SLAVE_NOT_AVAILABLE:消息发送成功,但Slave不可用,只有Master节点down机,消息才会丢失
  • 后三种状态,如果业务系统是可靠性消息投递,那么需要考虑补偿进行可靠性的重试投递

7、RocketMQ生产者-延迟消息

延迟消息:消息发到Broker后,要特定的时间才会被Consumer消费
目前RocketMQ只支持固定精度的定时消息
具体实现:

message.setDelayTimeLevel();

MessageStoreConfig配置类

//设置固定精度的消息延时投递的时间
private String messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h";

ScheduleMessageService任务类

public boolean parseDelayLevel() {
    HashMap<String, Long> timeUnitTable = new HashMap<String, Long>();
    timeUnitTable.put("s", 1000L);
    timeUnitTable.put("m", 1000L * 60);
    timeUnitTable.put("h", 1000L * 60 * 60);
    timeUnitTable.put("d", 1000L * 60 * 60 * 24);

    String levelString = this.defaultMessageStore.getMessageStoreConfig().getMessageDelayLevel();
    try {
        String[] levelArray = levelString.split(" ");
        for (int i = 0; i < levelArray.length; i++) {
            String value = levelArray[i];
            String ch = value.substring(value.length() - 1);
            Long tu = timeUnitTable.get(ch);

            int level = i + 1;
            if (level > this.maxDelayLevel) {
                this.maxDelayLevel = level;
            }
            long num = Long.parseLong(value.substring(0, value.length() - 1));
            long delayTimeMillis = tu * num;
            //解析并生成延时时间
            this.delayLevelTable.put(level, delayTimeMillis);
        }
    } catch (Exception e) {
        log.error("parseDelayLevel exception", e);
        log.info("levelString String = {}", levelString);
        return false;
    }

    return true;
}

8、RocketMQ生产者-自定义消息发送规则

将消息发送到指定队列(MessageQueue)
MessageQueueSelector:用于选择指定队列

public interface MessageQueueSelector {
    MessageQueue select(final List<MessageQueue> mqs, final Message msg, final Object arg);
}

要将消息发送到指定队列,这需要手动实现
1、producer实现:
producer只需发送消息时调用如下方法即可

/**
 * 发送有序消息
 *
 * @param messageMap 消息数据
 * @param selector   队列选择器,发送时会回调
 * @param order      回调队列选择器时,此参数会传入队列选择方法,提供配需规则
 * @return 发送结果
 */
public Result<SendResult> send(Message msg, MessageQueueSelector selector, Object arg)

实现MessageQueueSelector,接口并重写select()方法

public static void asyncMsgCustomQueue() throws Exception {
    DefaultMQProducer producer = new DefaultMQProducer("test_quick_producer_name");
    producer.setNamesrvAddr(Const.NAMESER_ADDR);

    producer.start();
    producer.setSendMsgTimeout(10000);
    //1、创建消息
    Message message = new Message("test_quick_topic",  //主题
                                    "TagA",          //标签
                                    "keyA",          //用户自定义的key,唯一的标识
                                    ("hello RocketMq").getBytes());//消息体
    //2、将消息发送到指定队列
    producer.send(message, new MessageQueueSelector() {
        @Override
        public MessageQueue select(List<MessageQueue> list, Message message, Object arg) {
            Integer queueNumber = (Integer) arg;
            int size = list.size();
            int index = queueNumber % size;
            return list.get(index);
        }
    },1);

}

拓展:

源码研究RocketMQ主从同步机制(HA)

rocketmq问题汇总-如何将特定消息发送至特定queue,消费者从特定queue消费

参考:

rocketmq netty底层设计

rocketMq-延迟消息介绍

RocketMQ(1)-架构原理

Rocketmq原理&最佳实践

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270

推荐阅读更多精彩内容