分布式事务解决方案——最大努力通知

分布式事务理论:分布式事务

什么是最大努力通知

最大努力通知也是一种解决分布式事务的方案,下边是一个是充值的例子:

交互流程:

  • 1、账户系统调用充值系统接口
  • 2、充值系统完成支付处理向账户系统发起充值结果通知,若通知失败,则充值系统按策略进行重复通知
  • 3、账户系统接收到充值结果通知修改充值状态。
  • 4、账户系统未接收到通知会主动调用充值系统的接口查询充值结果。
通过上边的例子我们总结最大努力通知方案的目标:

目标:发起通知方通过一定的机制最大努力将业务处理结果通知到接收方。

具体包括:
1、有一定的消息重复通知机制。

因为接收通知方可能没有接收到通知,此时要有一定的机制对消息重复通知。

2、消息校对机制。

如果尽最大努力也没有通知到接收方,或者接收方消费消息后要再次消费,此时可由接收方主动向通知方查询消息信息来满足需求。

最大努力通知与可靠消息一致性有什么不同?

1、解决方案思想不同

可靠消息一致性:发起通知方需要保证将消息发出去,并且将消息发到接收通知方,消息的可靠性关键由发起通知方来保证。

最大努力通知:发起通知方尽最大的努力将业务处理结果通知为接收通知方,但是可能消息接收不到,此时需要接收通知方主动调用发起通知方的接口查询业务处理结果,通知的可靠性关键在接收通知方。

2、两者的业务应用场景不同

可靠消息一致性:关注的是交易过程的事务一致,以异步的方式完成交易。

最大努力通知:关注的是交易后的通知事务,即将交易结果可靠的通知出去。

3、技术解决方向不同

可靠消息一致性:要解决消息从发出到接收的一致性,即消息发出并且被接收到。

最大努力通知:无法保证消息从发出到接收的一致性,只提供消息接收的可靠性机制。可靠机制是,最大努力的将消息通知给接收方,当消息无法被接收方接收时,由接收方主动查询消息(业务处理结果)。

解决方案

通过对最大努力通知的理解,采用MQ的ack机制就可以实现最大努力通知。

方案1:

本方案是利用MQ的ack机制由MQ向接收通知方发送通知,流程如下:

  • 1、发起通知方将通知发给MQ。
    使用普通消息机制将通知发给MQ。
    注意:如果消息没有发出去可由接收通知方主动请求发起通知方查询业务执行结果(后边会讲)。

  • 2、接收通知方监听 MQ。

  • 3、接收通知方接收消息,业务处理完成回应ack。

  • 4、接收通知方若没有回应ack则MQ会重复通知。
    MQ会按照间隔1min、5min、10min、30min、1h、2h、5h、10h的方式,逐步拉大通知间隔 (如果MQ采用rocketMq,在broker中可进行配置),直到达到通知要求的时间窗口上限。

  • 5、接收通知方可通过消息校对接口来校对消息的一致性。

方案2:

本方案也是利用MQ的ack机制,与方案1不同的是应用程序向接收通知方发送通知,如下图:

交互流程如下:

  • 1、发起通知方将通知发给MQ。
    使用可靠消息一致方案中的事务消息保证本地事务与消息的原子性,最终将通知先发给MQ。

  • 2、通知程序监听 MQ,接收MQ的消息。
    方案1中接收通知方直接监听MQ,方案2中由通知程序监听MQ。
    通知程序若没有回应ack则MQ会重复通知。

  • 3、通知程序通过互联网接口协议(如http、webservice)调用接收通知方案接口,完成通知。
    通知程序调用接收通知方案接口成功就表示通知成功,即消费MQ消息成功,MQ将不再向通知程序投递通知消息。

  • 4、接收通知方可通过消息校对接口来校对消息的一致性。

方案1和方案2的不同点:
  • 1、方案1中接收通知方与MQ接口,即接收通知方案监听 MQ,此方案主要应用与内部应用之间的通知。

  • 2、方案2中由通知程序与MQ接口,通知程序监听MQ,收到MQ的消息后由通知程序通过互联网接口协议调用接收通知方。此方案主要应用于外部应用之间的通知,例如支付宝、微信的支付结果(notify接口回调)通知。

RocketMQ实现最大努力通知型事务

业务说明

本实例通过RocketMq中间件实现最大努力通知型分布式事务,模拟充值过程。
本案例有账户系统和充值系统两个微服务,其中账户系统的数据库是bank1数据库,其中有张三账户。充值系统的数据库使用bank1_pay数据库,记录了账户的充值记录。

业务流程如下图:


交互流程如下:

  • 1、用户请求充值系统进行充值。

  • 2、充值系统完成充值将充值结果发给MQ。

  • 3、账户系统监听MQ,接收充值结果通知,如果接收不到消息,MQ会重复发送通知。接收到充值结果通知账户系统增加充值金额。

  • 4、账户系统也可以主动查询充值系统的充值结果查询接口,增加金额。

程序组成部分

本示例程序组成部分如下:
数据库:MySQL-5.7.25
包括bank1和bank1_pay两个数据库。
rocketmq 服务端:RocketMQ-4.5.0
rocketmq 客户端:RocketMQ-Spring-Boot-starter.2.0.2-RELEASE

微服务框架:
Nacos-Server:1.3.1
SpringBoot:2.2.10.RELEASE
spring-cloud-dependencies:Hoxton.SR8
spring-cloud-alibaba-dependencies:2.2.1.RELEASE

微服务及数据库的关系 :
rocket-notifymsg-demo-bank1 银行1,操作张三账户, 连接数据库bank1
rocket-notifymsg-demo-pay 银行2,操作充值记录,连接数据库bank1_pay

交互流程如下:

  • 1、用户请求充值系统进行充值。

  • 2、充值系统完成充值将充值结果发给MQ。

  • 3、账户系统监听MQ,接收充值结果通知,如果接收不到消息,MQ会重复发送通知。接收到充值结果通知账户系统增加充值金额。

  • 4、账户系统也可以主动查询充值系统的充值结果查询接口,增加金额。

创建数据库:

创建bank1库

CREATE DATABASE `bank1` CHARACTER SET 'utf8' COLLATE 'utf8_general_ci';

DROP TABLE IF EXISTS `account_info`;
CREATE TABLE `account_info` (
    `id` bigint(20) NOT NULL AUTO_INCREMENT,
    `account_name` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '户
    主姓名',
    `account_no` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '银行卡号',
    `account_password` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT
    '帐户密码',
    `account_balance` double NULL DEFAULT NULL COMMENT '帐户余额',
    PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;

INSERT INTO `account_info` VALUES (2, '张三的账户', '1', '', 10000);

DROP TABLE IF EXISTS `de_duplication`;
CREATE TABLE `de_duplication` (
    `tx_no` varchar(64) COLLATE utf8_bin NOT NULL,
    `create_time` datetime(0) NULL DEFAULT NULL,
    PRIMARY KEY (`tx_no`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;

创建bank1_pay库,并导入以下表结构:

CREATE DATABASE `bank1_pay` CHARACTER SET 'utf8' COLLATE 'utf8_general_ci';
CREATE TABLE `account_pay` (
    `id` varchar(64) COLLATE utf8_bin NOT NULL,
    `account_no` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '账号',
    `pay_amount` double NULL DEFAULT NULL COMMENT '充值余额',
    `result` varchar(20) COLLATE utf8_bin DEFAULT NULL COMMENT '充值结果:success,fail',
    PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;
启动RocketMQ
Windows系统:
set ROCKETMQ_HOME=[rocketmq服务端解压路径]
start [rocketmq服务端解压路径]/bin/mqnamesrv.cmd

Centos系统:
 进入rocketMQ解压目录下的bin文件夹
nohup sh bin/mqnamesrv & 
日志目录:{rocketMQ解压目录}/logs/rocketmqlogs/namesrv.log

启动broker:

Windows系统:
set ROCKETMQ_HOME=[rocketmq服务端解压路径]
start [rocketmq服务端解压路径]/bin/mqbroker.cmd ‐n 127.0.0.1:9876 autoCreateTopicEnable=true

Centos系统:
进入rocketMQ解压目录下的bin文件夹
nohup sh bin/mqbroker &
日志目录:{rocketMQ解压目录}/logs/rocketmqlogs/broker.log
创建

rocket-notifymsg-demo-bank1:银行1,操作张三账户, 连接数据库bank1
rocket-notifymsg-demo-pay:银行2,操作充值记录,连接数据库bank1_pay

引入maven依赖:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.2.10.RELEASE</version>
    <relativePath/>
</parent>

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <java.version>1.8</java.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>

    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        <exclusions>
            <exclusion>
                <groupId>com.alibaba.nacos</groupId>
                <artifactId>nacos-client</artifactId>
            </exclusion>
        </exclusions>
    </dependency>

    <dependency>
        <groupId>com.alibaba.nacos</groupId>
        <artifactId>nacos-client</artifactId>
        <version>1.3.1</version>
    </dependency>


    <dependency>
        <groupId>org.apache.rocketmq</groupId>
        <artifactId>rocketmq-spring-boot-starter</artifactId>
        <version>2.1.1</version>
    </dependency>

    <dependency>
        <groupId>tk.mybatis</groupId>
        <artifactId>mapper-spring-boot-starter</artifactId>
        <version>2.1.5</version>
    </dependency>

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.18</version>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.12</version>
        <scope>provided</scope>
    </dependency>

    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.74</version>
    </dependency>
</dependencies>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Hoxton.SR8</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-dependencies</artifactId>
            <version>2.2.1.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>

        <plugin>
            <groupId>org.mybatis.generator</groupId>
            <artifactId>mybatis-generator-maven-plugin</artifactId>
            <version>1.3.6</version>
            <configuration>
                <configurationFile>
                    ${basedir}/src/main/resources/generator/generatorConfig.xml
                </configurationFile>
                <overwrite>true</overwrite>
                <verbose>true</verbose>
            </configuration>
            <dependencies>
                <dependency>
                    <groupId>mysql</groupId>
                    <artifactId>mysql-connector-java</artifactId>
                    <version>5.1.41</version>
                </dependency>
                <dependency>
                    <groupId>tk.mybatis</groupId>
                    <artifactId>mapper</artifactId>
                    <version>4.1.5</version>
                </dependency>
            </dependencies>
        </plugin>
    </plugins>
</build>

rocket-notifymsg-demo-pay

rocket-notifymsg-demo-pay实现如下功能:

  • 1、充值接口
  • 2、充值完成要通知
  • 3、充值结果查询接口
application.properties
spring.application.name=notify-msg-pay
server.port=8094
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848

spring.datasource.driver-class-name = com.mysql.cj.jdbc.Driver
spring.datasource.url = jdbc:mysql://localhost:3306/trade_bank1_pay?useUnicode=true&useAffectedRows=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC
spring.datasource.username = root
spring.datasource.password = mysql


rocketmq.producer.group = producer_notifymsg_pay
rocketmq.name-server = 127.0.0.1:9876

logging.level.root = info
logging.level.org.springframework.web = info
logging.level.cn.itcast.wanxintx.effortdemo  = debug
Controller
@RestController
@RequestMapping("/account")
public class AccountPayController {

    @Autowired
    private AccountPayService accountPayService;

    //充值
    @GetMapping(value = "/paydo")
    public AccountPay pay(AccountPay accountPay){
        //生成事务编号
        String txNo = UUID.randomUUID().toString();
        accountPay.setId(txNo);
        return accountPayService.insertAccountPay(accountPay);
    }

    //查询充值结果
    @GetMapping(value = "/payResult/{txNo}")
    public AccountPay payresult(@PathVariable("txNo") String txNo){
        return accountPayService.getAccountPay(txNo);
    }
}
Service
@Service
@Slf4j
public class AccountPayService {

    @Autowired
    private AccountPayMapper accountPayMapper;

    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    /**
     * 插入充值记录
     * @param accountPay
     * @return
     */
    public AccountPay insertAccountPay(AccountPay accountPay) {
        int success = accountPayMapper.insertAccountPay(accountPay.getId(), accountPay.getAccountNo(), accountPay.getPayAmount(), "success");
        if(success>0){
            //发送通知,使用普通消息发送通知
            accountPay.setResult("success");
            rocketMQTemplate.convertAndSend("topic_notifymsg",accountPay);
            return accountPay;
        }
        return null;
    }

    /**
     * 查询充值记录,接收通知方调用此方法来查询充值结果
     * @param txNo
     * @return
     */
    public AccountPay getAccountPay(String txNo) {
        AccountPay accountPay = accountPayMapper.findByIdTxNo(txNo);
        return accountPay;
    }
}
Mapper
public interface AccountPayMapper extends Mapper<AccountPay> {


    int insertAccountPay(@Param("id") String id, @Param("accountNo") String accountNo, @Param("payAmount") Long pay_amount, @Param("result") String result);


    AccountPay findByIdTxNo(@Param("txNo") String txNo);
}

<mapper namespace="com.yibo.notifypay.mapper.AccountPayMapper">
  <resultMap id="BaseResultMap" type="com.yibo.notifypay.domain.entity.AccountPay">
    <!--
      WARNING - @mbg.generated
    -->
    <id column="id" jdbcType="VARCHAR" property="id" />
    <result column="account_no" jdbcType="VARCHAR" property="accountNo" />
    <result column="pay_amount" jdbcType="BIGINT" property="payAmount" />
    <result column="result" jdbcType="VARCHAR" property="result" />
  </resultMap>

  <insert id="insertAccountPay">
    insert into account_pay(id,account_no,pay_amount,result) values(#{id},#{accountNo},#{payAmount},#{result})
  </insert>

  <select id="findByIdTxNo" resultType="BaseResultMap">
    select id,account_no accountNo,pay_amount payAmount,result from account_pay where id=#{txNo}
  </select>

</mapper>

rocket-notifymsg-demo-bank1

rocket-notifymsg-demo-bank1实现如下功能:

  • 1、监听MQ,接收充值结果,根据充值结果完成账户金额修改。
  • 2、主动查询充值系统,根据充值结果完成账户金额修改。
application.properties
spring.application.name=notify-msg-bank1
server.port=8096
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848

spring.datasource.driver-class-name = com.mysql.cj.jdbc.Driver
spring.datasource.url = jdbc:mysql://localhost:3306/trade_bank1?useUnicode=true&useAffectedRows=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC
spring.datasource.username = root
spring.datasource.password = mysql


rocketmq.producer.group = producer_notifymsg_pay
rocketmq.name-server = 127.0.0.1:9876

logging.level.root = info
logging.level.org.springframework.web = info
logging.level.cn.itcast.wanxintx.effortdemo  = debug
Controller
@RestController
@Slf4j
public class AccountInfoController {

    @Autowired
    private AccountInfoService accountInfoService;

    //主动查询充值结果
    @GetMapping(value = "/payresult/{txNo}")
    public AccountPay result(@PathVariable("txNo") String txNo){
        AccountPay accountPay = accountInfoService.queryPayResult(txNo);
        return accountPay;
    }
}
Service
@Service
@Slf4j
public class AccountInfoService {

    @Autowired
    private AccountInfoMapper accountInfoMapper;

    @Autowired
    private PayClient payClient;

    /**
     * 更新账户金额
     * @param accountChange
     */
    @Transactional
    public void updateAccountBalance(AccountChangeEvent accountChange) {
        //幂等校验
        if(accountInfoMapper.isExistTx(accountChange.getTxNo())>0){
            return ;
        }
        int i = accountInfoMapper.updateAccountBalance(accountChange.getAccountNo(), accountChange.getAmount());
        //插入事务记录,用于幂等控制
        accountInfoMapper.addTx(accountChange.getTxNo());
    }

    /**
     * 远程调用查询充值结果
     * @param tx_no
     * @return
     */
    public AccountPay queryPayResult(String tx_no) {

        //远程调用
        AccountPay payresult = payClient.payresult(tx_no);
        if("success".equals(payresult.getResult())){
            //更新账户金额
            AccountChangeEvent accountChangeEvent = new AccountChangeEvent();
            accountChangeEvent.setAccountNo(payresult.getAccountNo());//账号
            accountChangeEvent.setAmount(payresult.getPayAmount());//金额
            accountChangeEvent.setTxNo(payresult.getId());//充值事务号
            updateAccountBalance(accountChangeEvent);
        }
        return payresult;
    }
}
Consumer
@Component
@Slf4j
@RocketMQMessageListener(topic = "topic_notifymsg",consumerGroup = "consumer_group_notifymsg_bank1")
public class NotifyMsgConsumer implements RocketMQListener<AccountPay> {

    @Autowired
    private AccountInfoService accountInfoService;

    @Override
    public void onMessage(AccountPay accountPay) {
        log.info("接收到消息:{}", JSON.toJSONString(accountPay));
        if("success".equals(accountPay.getResult())){
            //更新账户金额
            AccountChangeEvent accountChangeEvent = new AccountChangeEvent();
            accountChangeEvent.setAccountNo(accountPay.getAccountNo());
            accountChangeEvent.setAmount(accountPay.getPayAmount());
            accountChangeEvent.setTxNo(accountPay.getId());
            accountInfoService.updateAccountBalance(accountChangeEvent);
        }
        log.info("处理消息完成:{}", JSON.toJSONString(accountPay));
    }
}
Feign
@FeignClient(value = "notify-msg-pay")
public interface PayClient {

    //远程调用充值系统的接口查询充值结果
    @GetMapping(value = "account/payResult/{txNo}")
    public AccountPay payresult(@PathVariable("txNo") String txNo);
}
Mapper
public interface AccountInfoMapper extends Mapper<AccountInfo> {

    //修改账户金额
    int updateAccountBalance(@Param("accountNo") String accountNo, @Param("amount") Long amount);

    //查询幂等记录,用于幂等控制
    int isExistTx(String txNo);

    //添加事务记录,用于幂等控制
    int addTx(String txNo);
}

<mapper namespace="com.yibo.notify.mapper.AccountInfoMapper">
  <resultMap id="BaseResultMap" type="com.yibo.notify.domain.entity.AccountInfo">
    <!--
      WARNING - @mbg.generated
    -->
    <id column="id" jdbcType="BIGINT" property="id" />
    <result column="account_name" jdbcType="VARCHAR" property="accountName" />
    <result column="account_no" jdbcType="VARCHAR" property="accountNo" />
    <result column="account_password" jdbcType="VARCHAR" property="accountPassword" />
    <result column="account_balance" jdbcType="BIGINT" property="accountBalance" />
  </resultMap>


  <!-- 修改账户金额 -->
  <update id="updateAccountBalance">
    update account_info set account_balance=account_balance+#{amount} where account_no=#{accountNo}
  </update>


  <!-- 查询幂等记录,用于幂等控制 -->
  <select id="isExistTx">
    select count(1) from de_duplication where tx_no = #{txNo}
  </select>


  <!-- 添加事务记录,用于幂等控制 -->
  <insert id="addTx">
    insert into de_duplication values(#{txNo},now())
  </insert>

</mapper>

总结

最大努力通知方案是分布式事务中对一致性要求最低的一种,适用于一些最终一致性时间敏感度低的业务。

最大努力通知方案需要实现如下功能:

  • 1、消息重复通知机制。
  • 2、消息校对机制。

github源码地址:https://github.com/jjhyb/distributed-transaction

参考:
https://www.cnblogs.com/zeussbook/p/11799017.html

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