從源碼的角度解析Mybatis的會(huì)話機(jī)制

坐在我旁邊的鐘同學(xué)聽(tīng)說(shuō)我精通Mybatis源碼(我就想不通艾船,是誰(shuí)透漏了風(fēng)聲)疗我,就順帶問(wèn)了我一個(gè)問(wèn)題:在同一個(gè)方法中饮潦,Mybatis多次請(qǐng)求數(shù)據(jù)庫(kù)扼雏,是否要?jiǎng)?chuàng)建多個(gè)SqlSession會(huì)話坚嗜?

可能最近擼多了,當(dāng)時(shí)腦子里一片模糊诗充,眼神迷離苍蔬,雖然我當(dāng)時(shí)回答他:如果多個(gè)請(qǐng)求同一個(gè)事務(wù)中,那么多個(gè)請(qǐng)求都在共用一個(gè)SqlSession蝴蜓,反之每個(gè)請(qǐng)求都會(huì)創(chuàng)建一個(gè)SqlSession碟绑。這是我們?cè)谄匠i_(kāi)發(fā)中都習(xí)以為常的常識(shí)了,但我卻沒(méi)有從原理的角度給鐘同學(xué)分析,導(dǎo)致鐘同學(xué)茶飯不思蜈敢,作為老司機(jī)的我辜荠,感到深深的自責(zé),于是我暗自下定決心抓狭,要給鐘同學(xué)一個(gè)交代伯病。

不服跑個(gè)demo

測(cè)試在方法中不加事務(wù)時(shí),每個(gè)請(qǐng)求是否會(huì)創(chuàng)建一個(gè)SqlSession:

image

從日志可以看出否过,在沒(méi)有加事務(wù)的情況下午笛,確實(shí)是Mapper的每次請(qǐng)求數(shù)據(jù)庫(kù),都會(huì)創(chuàng)建一個(gè)SqlSession與數(shù)據(jù)庫(kù)交互苗桂,下面我們?cè)倏纯醇恿耸聞?wù)的情況:

image

從日志可以看出药磺,在方法中加了事務(wù)后,兩次請(qǐng)求只創(chuàng)建了一個(gè)SqlSession煤伟,再次證明了我上面的回答癌佩,但是僅僅這樣回答是體現(xiàn)完全不出一個(gè)老司機(jī)應(yīng)有的職業(yè)素養(yǎng)的,所以便锨,我要發(fā)車了围辙。

什么是SqlSession

在發(fā)車之前,我們必須得先搞明白放案,什么是SqlSession姚建?

簡(jiǎn)單來(lái)說(shuō),SqlSession是Mybatis工作的最頂層API會(huì)話接口吱殉,所有的數(shù)據(jù)庫(kù)操作都經(jīng)由它來(lái)實(shí)現(xiàn)掸冤,由于它就是一個(gè)會(huì)話,即一個(gè)SqlSession應(yīng)該僅存活于一個(gè)業(yè)務(wù)請(qǐng)求中友雳,也可以說(shuō)一個(gè)SqlSession對(duì)應(yīng)這一次數(shù)據(jù)庫(kù)會(huì)話稿湿,它不是永久存活的,每次訪問(wèn)數(shù)據(jù)庫(kù)時(shí)都需要?jiǎng)?chuàng)建它沥阱。

因此缎罢,SqlSession并不是線程安全,每個(gè)線程都應(yīng)該有它自己的 SqlSession 實(shí)例考杉,千萬(wàn)不能將一個(gè)SqlSession搞成單例形式策精,或者靜態(tài)域和實(shí)例變量的形式都會(huì)導(dǎo)致SqlSession出現(xiàn)事務(wù)問(wèn)題,這也就是為什么多個(gè)請(qǐng)求同一個(gè)事務(wù)中會(huì)共用一個(gè)SqlSession會(huì)話的原因崇棠,我們從SqlSession的創(chuàng)建過(guò)程來(lái)說(shuō)明這點(diǎn):

  1. 從Configuration配置類中拿到Environment數(shù)據(jù)源咽袜;
  2. 從數(shù)據(jù)源中獲取TransactionFactory和DataSource,并創(chuàng)建一個(gè)Transaction連接管理對(duì)象枕稀;
  3. 創(chuàng)建Executor對(duì)象(SqlSession只是所有操作的門面询刹,真正要干活的是Executor谜嫉,它封裝了底層JDBC所有的操作細(xì)節(jié));
  4. 創(chuàng)建SqlSession會(huì)話凹联。

每次創(chuàng)建一個(gè)SqlSession會(huì)話沐兰,都會(huì)伴隨創(chuàng)建一個(gè)專屬SqlSession的連接管理對(duì)象,如果SqlSession共享蔽挠,就會(huì)出現(xiàn)事務(wù)問(wèn)題住闯。

從源碼的角度分析

源碼分析從哪一步作為入口呢?如果是看過(guò)我之前寫的那幾篇關(guān)于mybatis的源碼分析澳淑,我相信你不會(huì)在Mybatis源碼前磨磨蹭蹭比原,遲遲找不到入口。

在之前的文章里已經(jīng)說(shuō)過(guò)了杠巡,Mapper的實(shí)現(xiàn)類是一個(gè)代理量窘,真正執(zhí)行邏輯的是MapperProxy.invoke(),該方法最終執(zhí)行的是sqlSessionTemplate氢拥。

org.mybatis.spring.SqlSessionTemplate:

private final SqlSession sqlSessionProxy;

public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
                          PersistenceExceptionTranslator exceptionTranslator) {

  notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
  notNull(executorType, "Property 'executorType' is required");

  this.sqlSessionFactory = sqlSessionFactory;
  this.executorType = executorType;
  this.exceptionTranslator = exceptionTranslator;
  this.sqlSessionProxy = (SqlSession) newProxyInstance(
    SqlSessionFactory.class.getClassLoader(),
    new Class[] { SqlSession.class },
    new SqlSessionInterceptor());
}

這個(gè)是創(chuàng)建SqlSessionTemplate的最終構(gòu)造方法蚌铜,可以看出sqlSessionTemplate中用到了SqlSession,是SqlSessionInterceptor實(shí)現(xiàn)的一個(gè)動(dòng)態(tài)代理類嫩海,所以我們直接深入要塞:

private class SqlSessionInterceptor implements InvocationHandler {
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    SqlSession sqlSession = getSqlSession(
      SqlSessionTemplate.this.sqlSessionFactory,
      SqlSessionTemplate.this.executorType,
      SqlSessionTemplate.this.exceptionTranslator);
    try {
      Object result = method.invoke(sqlSession, args);
      if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
        // force commit even on non-dirty sessions because some databases require
        // a commit/rollback before calling close()
        sqlSession.commit(true);
      }
      return result;
    } catch (Throwable t) {
      Throwable unwrapped = unwrapThrowable(t);
      if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
        // release the connection to avoid a deadlock if the translator is no loaded. See issue #22
        closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
        sqlSession = null;
        Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException) unwrapped);
        if (translated != null) {
          unwrapped = translated;
        }
      }
      throw unwrapped;
    } finally {
      if (sqlSession != null) {
        closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
      }
    }
  }
}

Mapper所有的方法厘线,最終都會(huì)用這個(gè)方法來(lái)處理所有的數(shù)據(jù)庫(kù)操作,茶飯不思的鐘同學(xué)眼神迷離不知道是不是自暴自棄導(dǎo)致擼多了出革,眼神空洞地望著我,問(wèn)我spring整合mybatis和mybatis單獨(dú)使用是否有區(qū)別渡讼,其實(shí)沒(méi)區(qū)別骂束,區(qū)別就是spring封裝了所有處理細(xì)節(jié),你就不用寫大量的冗余代碼成箫,專注于業(yè)務(wù)開(kāi)發(fā)展箱。

該動(dòng)態(tài)代理方法主要做了以下處理:

  1. 根據(jù)當(dāng)前條件獲取一個(gè)SqlSession,此時(shí)SqlSession可能是新創(chuàng)建的也有可能是獲取到上一次請(qǐng)求的SqlSession蹬昌;
  2. 反射執(zhí)行SqlSession方法混驰,再判斷當(dāng)前會(huì)話是否是一個(gè)事務(wù),如果是一個(gè)事務(wù)皂贩,則不commit栖榨;
  3. 如果此時(shí)拋出異常,判斷如果是PersistenceExceptionTranslator且不為空明刷,那么就關(guān)閉當(dāng)前會(huì)話婴栽,并且將sqlSession置為空防止finally重復(fù)關(guān)閉,PersistenceExceptionTranslator是spring定義的數(shù)據(jù)訪問(wèn)集成層的異常接口辈末;
  4. finally無(wú)論怎么執(zhí)行結(jié)果如何愚争,只要當(dāng)前會(huì)話不為空映皆,那么就會(huì)執(zhí)行關(guān)閉當(dāng)前會(huì)話操作,關(guān)閉當(dāng)前會(huì)話操作又會(huì)根據(jù)當(dāng)前會(huì)話是否有事務(wù)來(lái)決定會(huì)話是釋放還是直接關(guān)閉轰枝。

org.mybatis.spring.SqlSessionUtils#getSqlSession:

public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {

  notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
  notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);

  SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);

  SqlSession session = sessionHolder(executorType, holder);
  if (session != null) {
    return session;
  }

  if (LOGGER.isDebugEnabled()) {
    LOGGER.debug("Creating a new SqlSession");
  }

  session = sessionFactory.openSession(executorType);

  registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);

  return session;
}

是不是看到了不服跑個(gè)demo時(shí)看到的日志“Creating a new SqlSession”了捅彻,那么證明我直接深入的地方挺準(zhǔn)確的,沒(méi)有絲毫誤差鞍陨。在這個(gè)方法當(dāng)中步淹,首先是從TransactionSynchronizationManager(以下稱當(dāng)前線程事務(wù)管理器)獲取當(dāng)前線程threadLocal是否有SqlSessionHolder,如果有就從SqlSessionHolder取出當(dāng)前SqlSession湾戳,如果當(dāng)前線程threadLocal沒(méi)有SqlSessionHolder贤旷,就從sessionFactory中創(chuàng)建一個(gè)SqlSession,具體的創(chuàng)建步驟上面已經(jīng)說(shuō)過(guò)了砾脑,接著注冊(cè)會(huì)話到當(dāng)前線程threadLocal中幼驶。

先來(lái)看看當(dāng)前線程事務(wù)管理器的結(jié)構(gòu):

public abstract class TransactionSynchronizationManager {
  // ...
  // 存儲(chǔ)當(dāng)前線程事務(wù)資源,比如Connection韧衣、session等
  private static final ThreadLocal<Map<Object, Object>> resources =
    new NamedThreadLocal<>("Transactional resources");
  // 存儲(chǔ)當(dāng)前線程事務(wù)同步回調(diào)器
  // 當(dāng)有事務(wù)盅藻,該字段會(huì)被初始化,即激活當(dāng)前線程事務(wù)管理器
  private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
    new NamedThreadLocal<>("Transaction synchronizations");
  // ...
}

這是spring的一個(gè)當(dāng)前線程事務(wù)管理器畅铭,它允許將當(dāng)前資源存儲(chǔ)到當(dāng)前線程ThreadLocal中氏淑,從前面也可看出SqlSessionHolder是保存在resources中。

org.mybatis.spring.SqlSessionUtils#registerSessionHolder:

private static void registerSessionHolder(SqlSessionFactory sessionFactory, ExecutorType executorType,
                                          PersistenceExceptionTranslator exceptionTranslator, SqlSession session) {
  SqlSessionHolder holder;
  // 判斷當(dāng)前是否有事務(wù)
  if (TransactionSynchronizationManager.isSynchronizationActive()) {
    Environment environment = sessionFactory.getConfiguration().getEnvironment();
    // 判斷當(dāng)前環(huán)境配置的事務(wù)管理工廠是否是SpringManagedTransactionFactory(默認(rèn))
    if (environment.getTransactionFactory() instanceof SpringManagedTransactionFactory) {
      if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("Registering transaction synchronization for SqlSession [" + session + "]");
      }

      holder = new SqlSessionHolder(session, executorType, exceptionTranslator);
      // 綁定當(dāng)前SqlSessionHolder到線程ThreadLocal中
      TransactionSynchronizationManager.bindResource(sessionFactory, holder);
      // 注冊(cè)SqlSession同步回調(diào)器
      TransactionSynchronizationManager.registerSynchronization(new SqlSessionSynchronization(holder, sessionFactory));
      holder.setSynchronizedWithTransaction(true);
      // 會(huì)話使用次數(shù)+1
      holder.requested();
    } else {
      if (TransactionSynchronizationManager.getResource(environment.getDataSource()) == null) {
        if (LOGGER.isDebugEnabled()) {
          LOGGER.debug("SqlSession [" + session + "] was not registered for synchronization because DataSource is not transactional");
        }
      } else {
        throw new TransientDataAccessResourceException(
          "SqlSessionFactory must be using a SpringManagedTransactionFactory in order to use Spring transaction synchronization");
      }
    }
  } else {
    if (LOGGER.isDebugEnabled()) {
      LOGGER.debug("SqlSession [" + session + "] was not registered for synchronization because synchronization is not active");
    }
  }
}

注冊(cè)SqlSession到當(dāng)前線程事務(wù)管理器的條件首先是當(dāng)前環(huán)境中有事務(wù)硕噩,否則不注冊(cè)假残,判斷是否有事務(wù)的條件是synchronizations的ThreadLocal是否為空:

public static boolean isSynchronizationActive() {
  return (synchronizations.get() != null);
}

每當(dāng)我們開(kāi)啟一個(gè)事務(wù),會(huì)調(diào)用initSynchronization()方法進(jìn)行初始化synchronizations炉擅,以激活當(dāng)前線程事務(wù)管理器辉懒。

public static void initSynchronization() throws IllegalStateException {
  if (isSynchronizationActive()) {
    throw new IllegalStateException("Cannot activate transaction synchronization - already active");
  }
  logger.trace("Initializing transaction synchronization");
  synchronizations.set(new LinkedHashSet<TransactionSynchronization>());
}

所以當(dāng)前有事務(wù)時(shí),會(huì)注冊(cè)SqlSession到當(dāng)前線程ThreadLocal中谍失。

Mybatis自己也實(shí)現(xiàn)了一個(gè)自定義的事務(wù)同步回調(diào)器SqlSessionSynchronization眶俩,在注冊(cè)SqlSession的同時(shí),也會(huì)將SqlSessionSynchronization注冊(cè)到當(dāng)前線程事務(wù)管理器中快鱼,它的作用是根據(jù)事務(wù)的完成狀態(tài)回調(diào)來(lái)處理線程資源颠印,即當(dāng)前如果有事務(wù),那么當(dāng)每次狀態(tài)發(fā)生時(shí)就會(huì)回調(diào)事務(wù)同步器抹竹,具體細(xì)節(jié)可移步至Spring的org.springframework.transaction.support包线罕。

回到SqlSessionInterceptor代理類的邏輯,發(fā)現(xiàn)判斷會(huì)話是否需要提交要調(diào)用以下方法:

org.mybatis.spring.SqlSessionUtils#isSqlSessionTransactional:

public static boolean isSqlSessionTransactional(SqlSession session, SqlSessionFactory sessionFactory) {
  notNull(session, NO_SQL_SESSION_SPECIFIED);
  notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);

  SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);

  return (holder != null) && (holder.getSqlSession() == session);
}

取決于當(dāng)前SqlSession是否為空并且判斷當(dāng)前SqlSession是否與ThreadLocal中的SqlSession相等窃判,前面也分析了闻坚,如果當(dāng)前沒(méi)有事務(wù),SqlSession是不會(huì)保存到事務(wù)同步管理器的兢孝,即沒(méi)有事務(wù)窿凤,會(huì)話提交仅偎。

org.mybatis.spring.SqlSessionUtils#closeSqlSession:

public static void closeSqlSession(SqlSession session, SqlSessionFactory sessionFactory) {
  notNull(session, NO_SQL_SESSION_SPECIFIED);
  notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);

  SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
  if ((holder != null) && (holder.getSqlSession() == session)) {
    if (LOGGER.isDebugEnabled()) {
      LOGGER.debug("Releasing transactional SqlSession [" + session + "]");
    }
    holder.released();
  } else {
    if (LOGGER.isDebugEnabled()) {
      LOGGER.debug("Closing non transactional SqlSession [" + session + "]");
    }
    session.close();
  }
}

方法無(wú)論執(zhí)行結(jié)果如何都需要執(zhí)行關(guān)閉會(huì)話邏輯,這里的判斷也是判斷當(dāng)前是否有事務(wù)雳殊,如果SqlSession在事務(wù)當(dāng)中橘沥,則減少引用次數(shù),沒(méi)有真實(shí)關(guān)閉會(huì)話夯秃。如果當(dāng)前會(huì)話不存在事務(wù)座咆,則直接關(guān)閉會(huì)話。

寫在最后

雖說(shuō)鐘同學(xué)問(wèn)了我一個(gè)Mybatis的問(wèn)題仓洼,我卻中了Spring的圈套介陶,猛然發(fā)現(xiàn)整個(gè)事務(wù)鏈路都處在Spring的管控當(dāng)中,這里涉及到了Spring的自定義事務(wù)的一些機(jī)制色建,其中當(dāng)前線程事務(wù)管理器是整個(gè)事務(wù)的核心與中軸哺呜,當(dāng)前有事務(wù)時(shí),會(huì)初始化當(dāng)前線程事務(wù)管理器的synchronizations箕戳,即激活了當(dāng)前線程同步管理器某残,當(dāng)Mybatis訪問(wèn)數(shù)據(jù)庫(kù)會(huì)首先從當(dāng)前線程事務(wù)管理器獲取SqlSession,如果不存在就會(huì)創(chuàng)建一個(gè)會(huì)話陵吸,接著注冊(cè)會(huì)話到當(dāng)前線程事務(wù)管理器中玻墅,如果當(dāng)前有事務(wù),則會(huì)話不關(guān)閉也不commit壮虫,Mybatis還自定義了一個(gè)TransactionSynchronization澳厢,用于事務(wù)每次狀態(tài)發(fā)生時(shí)回調(diào)處理。

原作者:objcoding
原文鏈接:http://objcoding.com/2019/03/20/mybatis-sqlsession/
著作權(quán)歸作者所有囚似,任何形式的轉(zhuǎn)載都請(qǐng)聯(lián)系作者赏酥。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市谆构,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌框都,老刑警劉巖搬素,帶你破解...
    沈念sama閱讀 212,383評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異魏保,居然都是意外死亡熬尺,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,522評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門谓罗,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)粱哼,“玉大人,你說(shuō)我怎么就攤上這事檩咱〗掖耄” “怎么了胯舷?”我有些...
    開(kāi)封第一講書人閱讀 157,852評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)绊含。 經(jīng)常有香客問(wèn)我桑嘶,道長(zhǎng),這世上最難降的妖魔是什么躬充? 我笑而不...
    開(kāi)封第一講書人閱讀 56,621評(píng)論 1 284
  • 正文 為了忘掉前任逃顶,我火速辦了婚禮,結(jié)果婚禮上充甚,老公的妹妹穿的比我還像新娘以政。我一直安慰自己,他們只是感情好伴找,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,741評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布盈蛮。 她就那樣靜靜地躺著,像睡著了一般疆瑰。 火紅的嫁衣襯著肌膚如雪眉反。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 49,929評(píng)論 1 290
  • 那天穆役,我揣著相機(jī)與錄音寸五,去河邊找鬼。 笑死耿币,一個(gè)胖子當(dāng)著我的面吹牛梳杏,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播淹接,決...
    沈念sama閱讀 39,076評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼十性,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了塑悼?” 一聲冷哼從身側(cè)響起劲适,我...
    開(kāi)封第一講書人閱讀 37,803評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎厢蒜,沒(méi)想到半個(gè)月后霞势,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,265評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡斑鸦,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,582評(píng)論 2 327
  • 正文 我和宋清朗相戀三年愕贡,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片巷屿。...
    茶點(diǎn)故事閱讀 38,716評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡固以,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出嘱巾,到底是詐尸還是另有隱情憨琳,我是刑警寧澤诫钓,帶...
    沈念sama閱讀 34,395評(píng)論 4 333
  • 正文 年R本政府宣布,位于F島的核電站栽渴,受9級(jí)特大地震影響尖坤,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜闲擦,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,039評(píng)論 3 316
  • 文/蒙蒙 一慢味、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧墅冷,春花似錦纯路、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,798評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至腔彰,卻和暖如春叫编,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背霹抛。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 32,027評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工搓逾, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人杯拐。 一個(gè)月前我還...
    沈念sama閱讀 46,488評(píng)論 2 361
  • 正文 我出身青樓霞篡,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親端逼。 傳聞我的和親對(duì)象是個(gè)殘疾皇子朗兵,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,612評(píng)論 2 350