Seata AT 模式啟動源碼分析

從上一篇文章「分布式事務(wù)中間件Seata的設(shè)計原理」講了下 Seata AT 模式的一些設(shè)計原理,從中也知道了 AT 模式的三個角色(RM洞辣、TM胚吁、TC)延窜,接下來我會更新 Seata 源碼分析系列文章恋脚。今天就來分析 Seata AT 模式在啟動的時候都做了哪些操作腺办。

客戶端啟動邏輯

TM 是負(fù)責(zé)整個全局事務(wù)的管理器,因此一個全局事務(wù)是由 TM 開啟的糟描,TM 有個全局管理類 GlobalTransaction怀喉,結(jié)構(gòu)如下:

io.seata.tm.api.GlobalTransaction

public interface GlobalTransaction {

  void begin() throws TransactionException;

  void begin(int timeout) throws TransactionException;

  void begin(int timeout, String name) throws TransactionException;

  void commit() throws TransactionException;

  void rollback() throws TransactionException;
  
  GlobalStatus getStatus() throws TransactionException;
  
  // ...
}

可以通過 GlobalTransactionContext 創(chuàng)建一個 GlobalTransaction,然后用 GlobalTransaction 進(jìn)行全局事務(wù)的開啟船响、提交躬拢、回滾等操作,因此我們直接用 API 方式使用 Seata AT 模式:

//init seata;
TMClient.init(applicationId, txServiceGroup);
RMClient.init(applicationId, txServiceGroup);
//trx
GlobalTransaction tx = GlobalTransactionContext.getCurrentOrCreate();
try {
  tx.begin(60000, "testBiz");
  // 事務(wù)處理
  // ...
  tx.commit();
} catch (Exception exx) {
  tx.rollback();
  throw exx;
}

如果每次使用全局事務(wù)都這樣寫见间,難免會造成代碼冗余估灿,我們的項目都是基于 Spring 容器,這時我們可以利用 Spring AOP 的特性缤剧,用模板模式把這些冗余代碼封裝模版里,參考 Mybatis-spring 也是做了這么一件事情域慷,那么接下來我們來分析一下基于 Spring 的項目啟動 Seata 并注冊全局事務(wù)時都做了哪些工作荒辕。

我們開啟一個全局事務(wù)是在方法上加上 @GlobalTransactional注解,Seata 的 Spring 模塊中犹褒,有個 GlobalTransactionScanner抵窒,它的繼承關(guān)系如下:

public class GlobalTransactionScanner extends AbstractAutoProxyCreator implements InitializingBean, ApplicationContextAware, DisposableBean {
  // ...
}

在基于 Spring 項目的啟動過程中,對該類會有如下初始化流程:

InitializingBean 的 afterPropertiesSet() 方法調(diào)用了 initClient() 方法:

io.seata.spring.annotation.GlobalTransactionScanner#initClient

TMClient.init(applicationId, txServiceGroup);
RMClient.init(applicationId, txServiceGroup);

對 TM 和 RM 做了初始化操作叠骑。

  • TM 初始化

io.seata.tm.TMClient#init

public static void init(String applicationId, String transactionServiceGroup) {
  // 獲取 TmRpcClient 實例
  TmRpcClient tmRpcClient = TmRpcClient.getInstance(applicationId, transactionServiceGroup);
  // 初始化 TM Client
  tmRpcClient.init();
}

調(diào)用 TmRpcClient.getInstance() 方法會獲取一個 TM 客戶端實例李皇,在獲取過程中,會創(chuàng)建 Netty 客戶端配置文件對象宙枷,以及創(chuàng)建 messageExecutor 線程池掉房,該線程池用于在處理各種與服務(wù)端的消息交互,在創(chuàng)建 TmRpcClient 實例時慰丛,創(chuàng)建 ClientBootstrap卓囚,用于管理 Netty 服務(wù)的啟停,以及 ClientChannelManager诅病,它是專門用于管理 Netty 客戶端對象池哪亿,Seata 的 Netty 部分配合使用了對象吃粥烁,后面在分析網(wǎng)絡(luò)模塊會講到。

io.seata.core.rpc.netty.AbstractRpcRemotingClient#init

public void init() {
  clientBootstrap.start();
  // 定時嘗試連接服務(wù)端
  timerExecutor.scheduleAtFixedRate(new Runnable() {
    @Override
    public void run() {
      clientChannelManager.reconnect(getTransactionServiceGroup());
    }
  }, SCHEDULE_INTERVAL_MILLS, SCHEDULE_INTERVAL_MILLS, TimeUnit.SECONDS);
  mergeSendExecutorService = new ThreadPoolExecutor(MAX_MERGE_SEND_THREAD,
                                                    MAX_MERGE_SEND_THREAD,
                                                    KEEP_ALIVE_TIME, TimeUnit.MILLISECONDS,
                                                    new LinkedBlockingQueue<>(),
                                                    new NamedThreadFactory(getThreadPrefix(), MAX_MERGE_SEND_THREAD));
  mergeSendExecutorService.submit(new MergedSendRunnable());
  super.init();
}

調(diào)用 TM 客戶端 init() 方法蝇棉,最終會啟動 netty 客戶端(此時還未真正啟動讨阻,在對象池被調(diào)用時才會被真正啟動);開啟一個定時任務(wù)篡殷,定時重新發(fā)送 RegisterTMRequest(RM 客戶端會發(fā)送 RegisterRMRequest)請求嘗試連接服務(wù)端钝吮,具體邏輯是在 NettyClientChannelManager 中的 channels 中緩存了客戶端 channel,如果此時 channels 不存在獲取已過期贴唇,那么就會嘗試連接服務(wù)端以重新獲取 channel 并將其緩存到 channels 中搀绣;開啟一條單獨線程,用于處理異步請求發(fā)送戳气,這里用得很巧妙链患,之后在分析網(wǎng)絡(luò)模塊在具體對其進(jìn)行分析。

io.seata.core.rpc.netty.AbstractRpcRemoting#init

public void init() {
  timerExecutor.scheduleAtFixedRate(new Runnable() {
    @Override
    public void run() {
      for (Map.Entry<Integer, MessageFuture> entry : futures.entrySet()) {
        if (entry.getValue().isTimeout()) {
          futures.remove(entry.getKey());
          entry.getValue().setResultMessage(null);
          if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("timeout clear future: {}", entry.getValue().getRequestMessage().getBody());
          }
        }
      }

      nowMills = System.currentTimeMillis();
    }
  }, TIMEOUT_CHECK_INTERNAL, TIMEOUT_CHECK_INTERNAL, TimeUnit.MILLISECONDS);
}

在 AbstractRpcRemoting 的 init 方法中瓶您,又是開啟了一個定時任務(wù)麻捻,該定時任務(wù)主要是用于定時清除 futures 已過期的 futrue,futures 是保存發(fā)送請求需要返回結(jié)果的 future 對象呀袱,該對象有個超時時間贸毕,過了超時時間就會自動拋異常,因此需要定時清除已過期的 future 對象夜赵。

  • RM 初始化

io.seata.rm.RMClient#init

public static void init(String applicationId, String transactionServiceGroup) {
  RmRpcClient rmRpcClient = RmRpcClient.getInstance(applicationId, transactionServiceGroup);
  rmRpcClient.setResourceManager(DefaultResourceManager.get());
  rmRpcClient.setClientMessageListener(new RmMessageListener(DefaultRMHandler.get()));
  rmRpcClient.init();
}

RmRpcClient.getInstance 處理邏輯與 TM 大致相同明棍;ResourceManager 是 RM 資源管理器,負(fù)責(zé)分支事務(wù)的注冊寇僧、提交摊腋、上報、以及回滾操作嘁傀,以及全局鎖的查詢操作兴蒸,DefaultResourceManager 會持有當(dāng)前所有的 RM 資源管理器,進(jìn)行統(tǒng)一調(diào)用處理细办,而 get() 方法主要是加載當(dāng)前的資源管理器橙凳,主要用了類似 SPI 的機(jī)制,進(jìn)行靈活加載笑撞,如下圖岛啸,Seata 會掃描 META-INF/services/ 目錄下的配置類并進(jìn)行動態(tài)加載。

ClientMessageListener 是 RM 消息處理監(jiān)聽器娃殖,用于負(fù)責(zé)處理從 TC 發(fā)送過來的指令值戳,并對分支進(jìn)行分支提交、分支回滾炉爆,以及 undo log 文件刪除操作堕虹;最后 init 方法跟 TM 邏輯也大體一致卧晓;DefaultRMHandler 封裝了 RM 分支事務(wù)的一些具體操作邏輯。

接下來再看看 wrapIfNecessary 方法究竟做了哪些操作赴捞。

io.seata.spring.annotation.GlobalTransactionScanner#wrapIfNecessary

protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
  // 判斷是否有開啟全局事務(wù)
  if (disableGlobalTransaction) {
    return bean;
  }
  try {
    synchronized (PROXYED_SET) {
      if (PROXYED_SET.contains(beanName)) {
        return bean;
      }
      interceptor = null;
      //check TCC proxy
      if (TCCBeanParserUtils.isTccAutoProxy(bean, beanName, applicationContext)) {
        //TCC interceptor, proxy bean of sofa:reference/dubbo:reference, and LocalTCC
        interceptor = new TccActionInterceptor(TCCBeanParserUtils.getRemotingDesc(beanName));
      } else {
        Class<?> serviceInterface = SpringProxyUtils.findTargetClass(bean);
        Class<?>[] interfacesIfJdk = SpringProxyUtils.findInterfaces(bean);

        // 判斷 bean 中是否有 GlobalTransactional 和 GlobalLock 注解
        if (!existsAnnotation(new Class[]{serviceInterface})
            && !existsAnnotation(interfacesIfJdk)) {
          return bean;
        }

        if (interceptor == null) {
          // 創(chuàng)建代理類
          interceptor = new GlobalTransactionalInterceptor(failureHandlerHook);
        }
      }

      LOGGER.info("Bean[{}] with name [{}] would use interceptor [{}]",
                  bean.getClass().getName(), beanName, interceptor.getClass().getName());
      if (!AopUtils.isAopProxy(bean)) {
        bean = super.wrapIfNecessary(bean, beanName, cacheKey);
      } else {
        AdvisedSupport advised = SpringProxyUtils.getAdvisedSupport(bean);
        // 執(zhí)行包裝目標(biāo)對象到代理對象  
        Advisor[] advisor = super.buildAdvisors(beanName, getAdvicesAndAdvisorsForBean(null, null, null));
        for (Advisor avr : advisor) {
          advised.addAdvisor(0, avr);
        }
      }
      PROXYED_SET.add(beanName);
      return bean;
    }
  } catch (Exception exx) {
    throw new RuntimeException(exx);
  }
}

GlobalTransactionScanner 繼承了 AbstractAutoProxyCreator逼裆,用于對 Spring AOP 支持,從代碼中可看出赦政,用GlobalTransactionalInterceptor 代替了被 GlobalTransactional 和 GlobalLock 注解的方法胜宇。

GlobalTransactionalInterceptor 實現(xiàn)了 MethodInterceptor:

io.seata.spring.annotation.GlobalTransactionalInterceptor#invoke

public Object invoke(final MethodInvocation methodInvocation) throws Throwable {
  Class<?> targetClass = methodInvocation.getThis() != null ? AopUtils.getTargetClass(methodInvocation.getThis()) : null;
  Method specificMethod = ClassUtils.getMostSpecificMethod(methodInvocation.getMethod(), targetClass);
  final Method method = BridgeMethodResolver.findBridgedMethod(specificMethod);

  final GlobalTransactional globalTransactionalAnnotation = getAnnotation(method, GlobalTransactional.class);
  final GlobalLock globalLockAnnotation = getAnnotation(method, GlobalLock.class);
  if (globalTransactionalAnnotation != null) {
    // 全局事務(wù)注解
    return handleGlobalTransaction(methodInvocation, globalTransactionalAnnotation);
  } else if (globalLockAnnotation != null) {
    // 全局鎖注解
    return handleGlobalLock(methodInvocation);
  } else {
    return methodInvocation.proceed();
  }
}

以上是代理方法執(zhí)行的邏輯邏輯,其中 handleGlobalTransaction() 方法里面調(diào)用了 TransactionalTemplate 模版:

io.seata.spring.annotation.GlobalTransactionalInterceptor#handleGlobalTransaction

private Object handleGlobalTransaction(final MethodInvocation methodInvocation,
                                       final GlobalTransactional globalTrxAnno) throws Throwable {
  try {
    return transactionalTemplate.execute(new TransactionalExecutor() {
      @Override
      public Object execute() throws Throwable {
        return methodInvocation.proceed();
      }
      @Override
      public TransactionInfo getTransactionInfo() {
        // ...
      }
    });
  } catch (TransactionalExecutor.ExecutionException e) {
    // ...
  }
}

handleGlobalTransaction() 方法執(zhí)行了就是 TransactionalTemplate 模版類的 execute 方法:

io.seata.tm.api.TransactionalTemplate#execute

public Object execute(TransactionalExecutor business) throws Throwable {
  // 1. get or create a transaction
  GlobalTransaction tx = GlobalTransactionContext.getCurrentOrCreate();

  // 1.1 get transactionInfo
  TransactionInfo txInfo = business.getTransactionInfo();
  if (txInfo == null) {
    throw new ShouldNeverHappenException("transactionInfo does not exist");
  }
  try {

    // 2. begin transaction
    beginTransaction(txInfo, tx);

    Object rs = null;
    try {

      // Do Your Business
      rs = business.execute();

    } catch (Throwable ex) {

      // 3.the needed business exception to rollback.
      completeTransactionAfterThrowing(txInfo,tx,ex);
      throw ex;
    }

    // 4. everything is fine, commit.
    commitTransaction(tx);

    return rs;
  } finally {
    //5. clear
    triggerAfterCompletion();
    cleanUp();
  }
}

以上是不是有一種似曾相識的感覺恢着?沒錯桐愉,以上就是我們使用 API 時經(jīng)常寫的冗余代碼,現(xiàn)在 Spring 通過代理模式掰派,把這些冗余代碼都封裝帶模版里面了从诲,它將那些冗余代碼統(tǒng)統(tǒng)封裝起來統(tǒng)一流程處理,并不需要你顯示寫出來了靡羡,有興趣的也可以去看看 Mybatis-spring 的源碼系洛,也是寫得非常精彩。

服務(wù)端處理邏輯

服務(wù)端收到客戶端的連接略步,那當(dāng)然是將其 channel 也緩存起來描扯,前面也說到客戶端會發(fā)送 RegisterRMRequest/RegisterTMRequest 請求給服務(wù)端,服務(wù)端收到后會調(diào)用 ServerMessageListener 監(jiān)聽器處理:

io.seata.core.rpc.ServerMessageListener

public interface ServerMessageListener {
  // 處理各種事務(wù)趟薄,如分支注冊绽诚、分支提交、分支上報杭煎、分支回滾等等
  void onTrxMessage(RpcMessage request, ChannelHandlerContext ctx, ServerMessageSender sender);
    // 處理 RM 客戶端的注冊連接
  void onRegRmMessage(RpcMessage request, ChannelHandlerContext ctx,
                      ServerMessageSender sender, RegisterCheckAuthHandler checkAuthHandler);
  // 處理 TM 客戶端的注冊連接
  void onRegTmMessage(RpcMessage request, ChannelHandlerContext ctx,
                      ServerMessageSender sender, RegisterCheckAuthHandler checkAuthHandler);
  // 服務(wù)端與客戶端保持心跳
  void onCheckMessage(RpcMessage request, ChannelHandlerContext ctx, ServerMessageSender sender)

}

ChannelManager 是服務(wù)端 channel 的管理器憔购,服務(wù)端每次和客戶端通信,都需要從 ChannelManager 中獲取客戶端對應(yīng)的 channel岔帽,它用于保存 TM 和 RM 客戶端 channel 的緩存結(jié)構(gòu)如下:

/**
 * resourceId -> applicationId -> ip -> port -> RpcContext
 */
private static final ConcurrentMap<String, ConcurrentMap<String, ConcurrentMap<String, ConcurrentMap<Integer,
RpcContext>>>>
  RM_CHANNELS = new ConcurrentHashMap<String, ConcurrentMap<String, ConcurrentMap<String, ConcurrentMap<Integer,
RpcContext>>>>();

/**
 * ip+appname,port
 */
private static final ConcurrentMap<String, ConcurrentMap<Integer, RpcContext>> TM_CHANNELS
  = new ConcurrentHashMap<String, ConcurrentMap<Integer, RpcContext>>();

以上的 Map 結(jié)構(gòu)有點復(fù)雜:

RM_CHANNELS:

  1. resourceId 指的是 RM client 的數(shù)據(jù)庫地址;
  2. applicationId 指的是 RM client 的服務(wù) Id导绷,比如 springboot 的配置 spring.application.name=account-service 中的 account-service 即是 applicationId犀勒;
  3. ip 指的是 RM client 服務(wù)地址;
  4. port 指的是 RM client 服務(wù)地址妥曲;
  5. RpcContext 保存了本次注冊請求的信息贾费。

TM_CHANNELS:

  1. ip+appname:這里的注釋應(yīng)該是寫錯了,應(yīng)該是 appname+ip檐盟,即 TM_CHANNELS 的 Map 結(jié)構(gòu)第一個 key 為 appname+ip褂萧;
  2. port:客戶端的端口號。

以下是 RM Client 注冊邏輯:

io.seata.core.rpc.ChannelManager#registerRMChannel

public static void registerRMChannel(RegisterRMRequest resourceManagerRequest, Channel channel)
  throws IncompatibleVersionException {
  Version.checkVersion(resourceManagerRequest.getVersion());
  // 將 ResourceIds 數(shù)據(jù)庫連接連接信息放入一個set中
  Set<String> dbkeySet = dbKeytoSet(resourceManagerRequest.getResourceIds());
  RpcContext rpcContext;
  // 從緩存中判斷是否有該channel信息
  if (!IDENTIFIED_CHANNELS.containsKey(channel)) {
    // 根據(jù)請求注冊信息葵萎,構(gòu)建 rpcContext
    rpcContext = buildChannelHolder(NettyPoolKey.TransactionRole.RMROLE, resourceManagerRequest.getVersion(),
                                    resourceManagerRequest.getApplicationId(), resourceManagerRequest.getTransactionServiceGroup(),
                                    resourceManagerRequest.getResourceIds(), channel);
    // 將 rpcContext 放入緩存中
    rpcContext.holdInIdentifiedChannels(IDENTIFIED_CHANNELS);
  } else {
    rpcContext = IDENTIFIED_CHANNELS.get(channel);
    rpcContext.addResources(dbkeySet);
  }
  if (null == dbkeySet || dbkeySet.isEmpty()) { return; }
  for (String resourceId : dbkeySet) {
    String clientIp;
    // 將請求信息存入 RM_CHANNELS 中导犹,這里用了 java8 的 computeIfAbsent 方法操作
    ConcurrentMap<Integer, RpcContext> portMap = RM_CHANNELS.computeIfAbsent(resourceId, resourceIdKey -> new ConcurrentHashMap<>())
      .computeIfAbsent(resourceManagerRequest.getApplicationId(), applicationId -> new ConcurrentHashMap<>())
      .computeIfAbsent(clientIp = getClientIpFromChannel(channel), clientIpKey -> new ConcurrentHashMap<>());
        // 將當(dāng)前 rpcContext 放入 portMap 中
    rpcContext.holdInResourceManagerChannels(resourceId, portMap);
    updateChannelsResource(resourceId, clientIp, resourceManagerRequest.getApplicationId());
  }
}

從以上代碼邏輯能夠看出唱凯,注冊 RM client 主要是將注冊請求信息,放入 RM_CHANNELS 緩存中谎痢,同時還會從 IDENTIFIED_CHANNELS 中判斷本次請求的 channel 是否已驗證過磕昼,IDENTIFIED_CHANNELS 的結(jié)構(gòu)如下:

private static final ConcurrentMap<Channel, RpcContext> IDENTIFIED_CHANNELS
  = new ConcurrentHashMap<>();

IDENTIFIED_CHANNELS 包含了所有 TM 和 RM 已注冊的 channel。

以下是 TM 注冊邏輯:

io.seata.core.rpc.ChannelManager#registerTMChannel

public static void registerTMChannel(RegisterTMRequest request, Channel channel)
  throws IncompatibleVersionException {
  Version.checkVersion(request.getVersion());
  // 根據(jù)請求注冊信息节猿,構(gòu)建 RpcContext
  RpcContext rpcContext = buildChannelHolder(NettyPoolKey.TransactionRole.TMROLE, request.getVersion(),
                                             request.getApplicationId(),
                                             request.getTransactionServiceGroup(),
                                             null, channel);
  // 將 RpcContext 放入 IDENTIFIED_CHANNELS 緩存中
  rpcContext.holdInIdentifiedChannels(IDENTIFIED_CHANNELS);
  // account-service:127.0.0.1:63353
  String clientIdentified = rpcContext.getApplicationId() + Constants.CLIENT_ID_SPLIT_CHAR
    + getClientIpFromChannel(channel);
  // 將請求信息存入 TM_CHANNELS 緩存中
  TM_CHANNELS.putIfAbsent(clientIdentified, new ConcurrentHashMap<Integer, RpcContext>());
  // 將上一步創(chuàng)建好的get出來票从,之后再將rpcContext放入這個map的value中
  ConcurrentMap<Integer, RpcContext> clientIdentifiedMap = TM_CHANNELS.get(clientIdentified);
  rpcContext.holdInClientChannels(clientIdentifiedMap);
}

TM client 的注冊大體類似,把本次注冊的信息放入對應(yīng)的緩存中保存滨嘱,但比 RM client 的注冊邏輯簡單一些峰鄙,主要是 RM client 會涉及分支事務(wù)資源的信息,需要注冊的信息也會比 TM client 多太雨。

更多精彩文章請關(guān)注作者維護(hù)的公眾號「后端進(jìn)階」吟榴,這是一個專注后端相關(guān)技術(shù)的公眾號。
關(guān)注公眾號并回復(fù)「后端」免費領(lǐng)取后端相關(guān)電子書籍躺彬。
歡迎分享煤墙,轉(zhuǎn)載請保留出處。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末宪拥,一起剝皮案震驚了整個濱河市仿野,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌她君,老刑警劉巖脚作,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異缔刹,居然都是意外死亡球涛,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進(jìn)店門校镐,熙熙樓的掌柜王于貴愁眉苦臉地迎上來亿扁,“玉大人,你說我怎么就攤上這事鸟廓〈幼#” “怎么了?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵引谜,是天一觀的道長牍陌。 經(jīng)常有香客問我,道長员咽,這世上最難降的妖魔是什么毒涧? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮贝室,結(jié)果婚禮上契讲,老公的妹妹穿的比我還像新娘仿吞。我一直安慰自己,他們只是感情好怀泊,可當(dāng)我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布茫藏。 她就那樣靜靜地躺著,像睡著了一般霹琼。 火紅的嫁衣襯著肌膚如雪务傲。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天枣申,我揣著相機(jī)與錄音售葡,去河邊找鬼。 笑死忠藤,一個胖子當(dāng)著我的面吹牛挟伙,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播模孩,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼尖阔,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了榨咐?” 一聲冷哼從身側(cè)響起介却,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎块茁,沒想到半個月后齿坷,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡数焊,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年永淌,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片佩耳。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡遂蛀,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出干厚,到底是詐尸還是另有隱情答恶,我是刑警寧澤,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布萍诱,位于F島的核電站,受9級特大地震影響污呼,放射性物質(zhì)發(fā)生泄漏裕坊。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一燕酷、第九天 我趴在偏房一處隱蔽的房頂上張望籍凝。 院中可真熱鬧周瞎,春花似錦、人聲如沸饵蒂。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽退盯。三九已至彼乌,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間渊迁,已是汗流浹背慰照。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留琉朽,地道東北人毒租。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像箱叁,于是被迫代替她去往敵國和親墅垮。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內(nèi)容