hbase-put流程剖析

    众所周知hbase是一个写性能非常优越的NOSQL,今天从源码中分析一下put操作到了region中是如何进行处理的。

    region中处理put请求的大致流程如下图所示:

put流程图

1)检查region状态并尝试获取region中读写锁中的读锁,1.检查读操作时是否可读。2.检查region是否rit状态,是则不允许read、split和merge操作。3.获取region读写锁中的读锁。4.如果region绑定了协处理器则将operation发送至协处理器。

2)检查memstore大小,该region上memstore总大小是否超过blockingMemStoreSize(hbase.hregion.memstore.block.multiplier*hbase.hregion.memstore.flush.size)的大小,超过则调用memstore flush并抛出RegionTooBusyException Above memstore limit异常。如果put操作的是meta所在的region则不进行memstore检查。

3)检查put中的数据,1.检查put中的的cf是否存在表中定义,不存在则会返回NoSuchColumnFamilyException。2.检查put中的timestamp是否大于hbase.hregion.keyvalue.timestamp.slop.millisecs+当前时间,大于则抛出FailedSanityCheckException Timestamp for KV out of range 异常。3.检查put中的rowkey是否在region的[starRowKey,endRowKey)范围内。

4)获取行级锁,获取第一个行级锁时会阻塞获取行级锁直到hbase.rowlock.wait.duration超时,获取非第一个行级锁时如果获取不到则会马上返回不会阻塞,这样可以减少前面已经获取到行级锁的锁占用时间。

5)修改写入数据的timestamp,将待写入数据的timestamp修改为当前时间。

6)获取region中updatesLock的读锁。

7)构建WAL Edit并写入Appen Hlog中,HBase使用WAL机制保证数据可靠性,即首先写日志再写缓存,即使发生宕机,也可以通过恢复HLog还原出原始数据。该步骤就是将数据构造为WALEdit对象,然后顺序写入HLog中,此时不需要执行sync操作。

8)将数据写入memstore,HBase中每列族都会对应一个store,用来存储该列数据。每个store都会有个写缓存memstore,用于缓存写入数据。HBase并不会直接将数据落盘,而是先写入缓存,等缓存满足一定大小之后再一起落盘。

9)释放region中updatesLock的读锁和行级锁。

10)sync wal,将Hlog真正写入hdfs中,释放锁之后才进行该步骤是为了减少锁的占用时间,提高写性能,如果Sync失败,执行回滚操作将memstore中已经写入的数据移除。

11)移动MVCC,此操作之后写入的数据才能被读操作(get、scan)获取到。

12)检查memstore size,memstore的大小是否大于hbase.hregion.memstore.flush.size,是则进行flush操作。

主要源码如下,已在源码中添加了注释,

OperationStatus[] batchMutate(BatchOperationInProgress batchOp) :

OperationStatus[] batchMutate(BatchOperationInProgress<?> batchOp) throws IOException {

  boolean initialized = false;

  Operation op = batchOp.isInReplay() ? Operation.REPLAY_BATCH_MUTATE : Operation.BATCH_MUTATE;

  //1.检查读操作时是否可读

  //2.检查region是否rit状态,是则不允许read、split和merge操作

  //3.获取region读写锁中的读锁

  //4.如果region绑定了协处理器则将operation发送至协处理器

  startRegionOperation(op);

  try {

    while (!batchOp.isDone()) {

      if (!batchOp.isInReplay()) {

        checkReadOnly();

      }

      //检查memstore的大小是否超过blockingMemStoreSize的大小,meta所在的region不检查。

      checkResources();

      if (!initialized) {

        this.writeRequestsCount.add(batchOp.operations.length);

        //非回放的操作,会先调用各自操作协处理器的pre方法。

        if (!batchOp.isInReplay()) {

          doPreMutationHook(batchOp);

        }

        initialized = true;

      }

    // 处理请求,主要处理逻辑在该方法中

      doMiniBatchMutation(batchOp);

      long newSize = this.getMemstoreSize();

      //判断memstore的大小是否大于hbase.hregion.memstore.flush.size,是则进行flush操作。

      if (isFlushSize(newSize)) {

        requestFlush();

      }

    }

  } finally {

    //1.释放region读写锁的读锁

    //2.如果region绑定了协处理器则将operation发送至协处理器

    closeRegionOperation(op);

  }

  return batchOp.retCodeDetails;

}

private long doMiniBatchMutation(BatchOperationInProgress batchOp):

private long doMiniBatchMutation(BatchOperationInProgress batchOp)throws IOException {

boolean isInReplay = batchOp.isInReplay();

  // variable to note if all Put items are for the same CF -- metrics related

//记录所有的批量操作中是不是否操作一样的cf,如果都是操作相同的cf,则可以将批量put操作转换为multiput进行优化。

  boolean putsCfSetConsistent =true;

  //The set of columnFamilies first seen for Put.

  Set putsCfSet =null;

  // variable to note if all Delete items are for the same CF -- metrics related

  boolean deletesCfSetConsistent =true;

  //The set of columnFamilies first seen for Delete.

  Set deletesCfSet =null;

  long currentNonceGroup = HConstants.NO_NONCE, currentNonce = HConstants.NO_NONCE;

  WALEdit walEdit =new WALEdit(isInReplay);

  MultiVersionConcurrencyControl.WriteEntry writeEntry =null;

  long txid =0;

  boolean doRollBackMemstore =false;

  boolean locked =false;

  /** Keep track of the locks we hold so we can release them in finally clause */

  //记录获取到的行级锁,方便进行释放。

  List acquiredRowLocks = Lists.newArrayListWithCapacity(batchOp.operations.length);

  // reference family maps directly so coprocessors can mutate them if desired

  Map>[] familyMaps =new Map[batchOp.operations.length];

  // We try to set up a batch in the range [firstIndex,lastIndexExclusive)

  int firstIndex = batchOp.nextIndexToProcess;

  int lastIndexExclusive = firstIndex;

  RowLock prevRowLock =null;

  boolean success =false;

  int noOfPuts =0, noOfDeletes =0;

  WALKey walKey =null;

  long mvccNum =0;

  long addedSize =0;

  try {

// ------------------------------------

// STEP 1. Try to acquire as many locks as we can, and ensure

// we acquire at least one.

// ----------------------------------

    int numReadyToWrite =0;

    long now = EnvironmentEdgeManager.currentTime();

    while (lastIndexExclusive < batchOp.operations.length) {

Mutation mutation = batchOp.getMutation(lastIndexExclusive);

      boolean isPutMutation = mutationinstanceof Put;

      Map> familyMap = mutation.getFamilyCellMap();

      // store the family map reference to allow for mutations

      familyMaps[lastIndexExclusive] = familyMap;

      // skip anything that "ran" already

      if (batchOp.retCodeDetails[lastIndexExclusive].getOperationStatusCode()

!= OperationStatusCode.NOT_RUN) {

lastIndexExclusive++;

continue;

      }

try {

if (isPutMutation) {

// Check the families in the put. If bad, skip this one.

//如果是数据回放 则移除数据中cf在htableDescriptor中不存在的

          if (isInReplay) {

removeNonExistentColumnFamilyForReplay(familyMap);

          }else {//如果是非回放数据,则检查htableDescriptor中是否存在需要操作的cf,不存在则会返回NoSuchColumnFamilyException

            checkFamilies(familyMap.keySet());

          }

//判读操作的keyvlue中的timestamp是否大于hbase.hregion.keyvalue.timestamp.slop.millisecs+当前时间,大于则抛出FailedSanityCheckException Timestamp for KV out of range 异常。

          checkTimestamps(mutation.getFamilyCellMap(), now);

        }else {

//删除操作,

// 如果是删除整行操作,则将htableDescriptor中定义的cf设置到Delete对象中进行删除整行。

// 如果不是删除整行操作,则检查Delete中的cf是否在htableDescriptor中定义。

          prepareDelete((Delete) mutation);

        }

//检查操作的rowkey是否在本region的[starRowKey,endRowKey)范围内。

        checkRow(mutation.getRow(), "doMiniBatchMutation");

      }catch (NoSuchColumnFamilyException nscf) {

LOG.warn("No such column family in batch mutation", nscf);

        batchOp.retCodeDetails[lastIndexExclusive] =new OperationStatus(

OperationStatusCode.BAD_FAMILY, nscf.getMessage());

        lastIndexExclusive++;

continue;

      }catch (FailedSanityCheckException fsce) {

LOG.warn("Batch Mutation did not pass sanity check", fsce);

        batchOp.retCodeDetails[lastIndexExclusive] =new OperationStatus(

OperationStatusCode.SANITY_CHECK_FAILURE, fsce.getMessage());

        lastIndexExclusive++;

continue;

      }catch (WrongRegionException we) {

LOG.warn("Batch mutation had a row that does not belong to this region", we);

        batchOp.retCodeDetails[lastIndexExclusive] =new OperationStatus(

OperationStatusCode.SANITY_CHECK_FAILURE, we.getMessage());

        lastIndexExclusive++;

continue;

      }

//HBASE-18233

// If we haven't got any rows in our batch, we should block to

// get the next one's read lock. We need at least one row to mutate.

// If we have got rows, do not block when lock is not available,

// so that we can fail fast and go on with the rows with locks in

// the batch. By doing this, we can reduce contention and prevent

// possible deadlocks.

// The unfinished rows in the batch will be detected in batchMutate,

// and it wil try to finish them by calling doMiniBatchMutation again.

// 获取行级锁。

// 当此次批处理中还没有获取到任何一个行的行级锁则会阻塞获取行级锁,直到hbase.rowlock.wait.duration超时

// 当此次批处理中已有获得的行级锁则会则会尝试获取本行的行级锁,如果获取不到本行行级锁则立马返回不阻塞,减少其他已获取的行级锁的占用时间也尽量避免了死锁。

// 获取不到行级锁则会跳出while循环,此次批处理就只处理已获取到行级锁的行,获取不到行级锁的操作会在下一次批处理中进行处理。

      boolean shouldBlock = numReadyToWrite ==0;

      RowLock rowLock =null;

      try {

//获取行级锁

        rowLock = getRowLockInternal(mutation.getRow(), true, shouldBlock, prevRowLock);

      }catch (IOException ioe) {

LOG.warn("Failed getting lock in batch put, row="

          + Bytes.toStringBinary(mutation.getRow()), ioe);

      }

//获取不到行级锁则跳出while循环

      if (rowLock ==null) {

// We failed to grab another lock. Stop acquiring more rows for this

// batch and go on with the gotten ones

        break;

      }else {

//当前获取的行级锁和上一个行级锁不一致,则会添加到已获取行级锁的集合中,方便对锁进行释放。

        if (rowLock != prevRowLock) {

// It is a different row now, add this to the acquiredRowLocks and

// set prevRowLock to the new returned rowLock

          acquiredRowLocks.add(rowLock);

          prevRowLock = rowLock;

        }

}

lastIndexExclusive++;

      numReadyToWrite++;

      if (isPutMutation) {

// If Column Families stay consistent through out all of the

// individual puts then metrics can be reported as a mutliput across

// column families in the first put.

        if (putsCfSet ==null) {

putsCfSet = mutation.getFamilyCellMap().keySet();

        }else {

putsCfSetConsistent = putsCfSetConsistent

&& mutation.getFamilyCellMap().keySet().equals(putsCfSet);

        }

}else {

if (deletesCfSet ==null) {

deletesCfSet = mutation.getFamilyCellMap().keySet();

        }else {

deletesCfSetConsistent = deletesCfSetConsistent

&& mutation.getFamilyCellMap().keySet().equals(deletesCfSet);

        }

}

}//while循环结束到这

// we should record the timestamp only after we have acquired the rowLock,

// otherwise, newer puts/deletes are not guaranteed to have a newer timestamp

// 在获取完行级锁之后获取当前时间,作为cell中的timestamp。

    now = EnvironmentEdgeManager.currentTime();

    byte[] byteNow = Bytes.toBytes(now);

    // Nothing to put/delete -- an exception in the above such as NoSuchColumnFamily?

//如果没有获取到任何一个行级锁则返回0。

//优化:这个判断可以放到获取当前时间前面,当没有获取到行级锁时少获取一次当前时间,能优化一点是一点

    if (numReadyToWrite <=0)return 0L;

    // We've now grabbed as many mutations off the list as we can

// ------------------------------------

// STEP 2. Update any LATEST_TIMESTAMP timestamps

// ----------------------------------

    for (int i = firstIndex; !isInReplay && i < lastIndexExclusive; i++) {

// skip invalid

      if (batchOp.retCodeDetails[i].getOperationStatusCode()

!= OperationStatusCode.NOT_RUN)continue;

      Mutation mutation = batchOp.getMutation(i);

      if (mutationinstanceof Put) {

//使用byteNow timestamp更新Put cell的timestamp。

        updateCellTimestamps(familyMaps[i].values(), byteNow);

        noOfPuts++;

      }else {

prepareDeleteTimestamps(mutation, familyMaps[i], byteNow);

        noOfDeletes++;

      }

//添加ttl

      rewriteCellTags(familyMaps[i], mutation);

    }

//获取region中updatesLock的读锁

    lock(this.updatesLock.readLock(), numReadyToWrite);

    locked =true;

    // calling the pre CP hook for batch mutation

    if (!isInReplay &&coprocessorHost !=null) {

MiniBatchOperationInProgress miniBatchOp =

new MiniBatchOperationInProgress(batchOp.getMutationsForCoprocs(),

        batchOp.retCodeDetails, batchOp.walEditsFromCoprocessors, firstIndex, lastIndexExclusive);

      if (coprocessorHost.preBatchMutate(miniBatchOp))return 0L;

    }

// ------------------------------------

// STEP 3. Build WAL edit

// ----------------------------------

    Durability durability = Durability.USE_DEFAULT;

    for (int i = firstIndex; i < lastIndexExclusive; i++) {

// Skip puts that were determined to be invalid during preprocessing

      if (batchOp.retCodeDetails[i].getOperationStatusCode() != OperationStatusCode.NOT_RUN) {

continue;

      }

Mutation m = batchOp.getMutation(i);

      //获取wal持久化级别SKIP_WAL、ASYNC_WAL、SYNC_WAL、FSYNC_WAL等

      Durability tmpDur = getEffectiveDurability(m.getDurability());

      if (tmpDur.ordinal() > durability.ordinal()) {

durability = tmpDur;

      }

if (tmpDur == Durability.SKIP_WAL) {

recordMutationWithoutWal(m.getFamilyCellMap());

continue;

      }

long nonceGroup = batchOp.getNonceGroup(i), nonce = batchOp.getNonce(i);

      // In replay, the batch may contain multiple nonces. If so, write WALEdit for each.

// Given how nonces are originally written, these should be contiguous.

// They don't have to be, it will still work, just write more WALEdits than needed.

      if (nonceGroup != currentNonceGroup || nonce != currentNonce) {

if (walEdit.size() >0) {

assert isInReplay;

          if (!isInReplay) {

throw new IOException("Multiple nonces per batch and not in replay");

          }

// txid should always increase, so having the one from the last call is ok.

// we use HLogKey here instead of WALKey directly to support legacy coprocessors.

          walKey =new ReplayHLogKey(this.getRegionInfo().getEncodedNameAsBytes(),

            this.htableDescriptor.getTableName(), now, m.getClusterIds(),

            currentNonceGroup, currentNonce, mvcc);

          txid =this.wal.append(this.htableDescriptor,  this.getRegionInfo(),  walKey,

            walEdit, true);

          walEdit =new WALEdit(isInReplay);

          walKey =null;

        }

currentNonceGroup = nonceGroup;

        currentNonce = nonce;

      }

// Add WAL edits by CP

      WALEdit fromCP = batchOp.walEditsFromCoprocessors[i];

      if (fromCP !=null) {

for (Cell cell : fromCP.getCells()) {

walEdit.add(cell);

        }

}

addFamilyMapToWALEdit(familyMaps[i], walEdit);

    }

// -------------------------

// STEP 4. Append the final edit to WAL. Do not sync wal.

// -------------------------

    Mutation mutation = batchOp.getMutation(firstIndex);

    if (isInReplay) {

// use wal key from the original

      walKey =new ReplayHLogKey(this.getRegionInfo().getEncodedNameAsBytes(),

        this.htableDescriptor.getTableName(), WALKey.NO_SEQUENCE_ID, now,

        mutation.getClusterIds(), currentNonceGroup, currentNonce, mvcc);

      long replaySeqId = batchOp.getReplaySequenceId();

      walKey.setOrigLogSeqNum(replaySeqId);

    }

if (walEdit.size() >0) {

if (!isInReplay) {

// we use HLogKey here instead of WALKey directly to support legacy coprocessors.

      walKey =new HLogKey(this.getRegionInfo().getEncodedNameAsBytes(),

          this.htableDescriptor.getTableName(), WALKey.NO_SEQUENCE_ID, now,

          mutation.getClusterIds(), currentNonceGroup, currentNonce, mvcc);

      }

//将waledit写入RingBufferTruck缓存中,并获取到txid

      txid =this.wal.append(this.htableDescriptor, this.getRegionInfo(), walKey, walEdit, true);

    }

// ------------------------------------

// Acquire the latest mvcc number

// ----------------------------------

// 获取最新的mvcc

    if (walKey ==null) {

// If this is a skip wal operation just get the read point from mvcc

      walKey =this.appendEmptyEdit(this.wal);

    }

if (!isInReplay) {

writeEntry = walKey.getWriteEntry();

      mvccNum = writeEntry.getWriteNumber();

    }else {

mvccNum = batchOp.getReplaySequenceId();

    }

// ------------------------------------

// STEP 5. Write back to memstore

// Write to memstore. It is ok to write to memstore

// first without syncing the WAL because we do not roll

// forward the memstore MVCC. The MVCC will be moved up when

// the complete operation is done. These changes are not yet

// visible to scanners till we update the MVCC. The MVCC is

// moved only when the sync is complete.

// ----------------------------------

    for (int i = firstIndex; i < lastIndexExclusive; i++) {

if (batchOp.retCodeDetails[i].getOperationStatusCode()

!= OperationStatusCode.NOT_RUN) {

continue;

      }

doRollBackMemstore =true; // If we have a failure, we need to clean what we wrote

      addedSize += applyFamilyMapToMemstore(familyMaps[i], mvccNum, isInReplay);

    }

// -------------------------------

// STEP 6. Release row locks, etc.

// -------------------------------

//释放锁

    if (locked) {

this.updatesLock.readLock().unlock();

      locked =false;

    }

releaseRowLocks(acquiredRowLocks);

    // -------------------------

// STEP 7. Sync wal.

// -------------------------

// 如果sync失败则会抛出IOException异常,从而直接跳到finally中并回滚已写入memstore中的数据。

    if (txid !=0) {

syncOrDefer(txid, durability);

    }

doRollBackMemstore =false;

    // calling the post CP hook for batch mutation

    if (!isInReplay &&coprocessorHost !=null) {

MiniBatchOperationInProgress miniBatchOp =

new MiniBatchOperationInProgress(batchOp.getMutationsForCoprocs(),

        batchOp.retCodeDetails, batchOp.walEditsFromCoprocessors, firstIndex, lastIndexExclusive);

      coprocessorHost.postBatchMutate(miniBatchOp);

    }

// ------------------------------------------------------------------

// STEP 8. Advance mvcc. This will make this put visible to scanners and getters.

// 此操作成功之后,写入的数据才可以被get、scan操作获取。

// ------------------------------------------------------------------

    if (writeEntry !=null) {

mvcc.completeAndWait(writeEntry);

      writeEntry =null;

    }else if (isInReplay) {

// ensure that the sequence id of the region is at least as big as orig log seq id

      mvcc.advanceTo(mvccNum);

    }

for (int i = firstIndex; i < lastIndexExclusive; i ++) {

if (batchOp.retCodeDetails[i] == OperationStatus.NOT_RUN) {

batchOp.retCodeDetails[i] = OperationStatus.SUCCESS;

      }

}

// ------------------------------------

// STEP 9. Run coprocessor post hooks. This should be done after the wal is

// synced so that the coprocessor contract is adhered to.

// ------------------------------------

    if (!isInReplay &&coprocessorHost !=null) {

for (int i = firstIndex; i < lastIndexExclusive; i++) {

// only for successful puts

        if (batchOp.retCodeDetails[i].getOperationStatusCode()

!= OperationStatusCode.SUCCESS) {

continue;

        }

Mutation m = batchOp.getMutation(i);

        if (minstanceof Put) {

coprocessorHost.postPut((Put) m, walEdit, m.getDurability());

        }else {

coprocessorHost.postDelete((Delete) m, walEdit, m.getDurability());

        }

}

}

success =true;

    return addedSize;

  }finally {

// if the wal sync was unsuccessful, remove keys from memstore

//sync 失败则将写入memstore的数据移除。

    if (doRollBackMemstore) {

for (int j =0; j < familyMaps.length; j++) {

for(List cells:familyMaps[j].values()) {

rollbackMemstore(cells);

        }

}

if (writeEntry !=null)mvcc.complete(writeEntry);

    }else {

this.addAndGetGlobalMemstoreSize(addedSize);

      if (writeEntry !=null) {

mvcc.completeAndWait(writeEntry);

      }

}

if (locked) {

this.updatesLock.readLock().unlock();

    }

releaseRowLocks(acquiredRowLocks);

    // See if the column families were consistent through the whole thing.

// if they were then keep them. If they were not then pass a null.

// null will be treated as unknown.

// Total time taken might be involving Puts and Deletes.

// Split the time for puts and deletes based on the total number of Puts and Deletes.

    if (noOfPuts >0) {

// There were some Puts in the batch.

      if (this.metricsRegion !=null) {

this.metricsRegion.updatePut();

      }

}

if (noOfDeletes >0) {

// There were some Deletes in the batch.

      if (this.metricsRegion !=null) {

this.metricsRegion.updateDelete();

      }

}

if (!success) {

for (int i = firstIndex; i < lastIndexExclusive; i++) {

if (batchOp.retCodeDetails[i].getOperationStatusCode() == OperationStatusCode.NOT_RUN) {

batchOp.retCodeDetails[i] = OperationStatus.FAILURE;

        }

}

}

if (coprocessorHost !=null && !batchOp.isInReplay()) {

// call the coprocessor hook to do any finalization steps

// after the put is done

      MiniBatchOperationInProgress miniBatchOp =

new MiniBatchOperationInProgress(batchOp.getMutationsForCoprocs(),

              batchOp.retCodeDetails, batchOp.walEditsFromCoprocessors, firstIndex,

              lastIndexExclusive);

      coprocessorHost.postBatchMutateIndispensably(miniBatchOp, success);

    }

batchOp.nextIndexToProcess = lastIndexExclusive;

  }

}

       今天的分享就到这,有看不明白的地方一定是我写的不够清楚,所有欢迎提任何问题以及改善方法。

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