kafka學習(3) 消費者:消費者組,重平衡,訂閱方式,開發(fā)步驟

KafkaConsumer負責訂閱主題, 并且從訂閱的主題中拉取消息。

消費者和消費組

每一個消費者都有一個對應的消費組, 當消息發(fā)布到主題之后, 只會被投遞給訂閱它的每個消費組中的一個消費者。消費組將消費者歸為一類, 每一個消費者只隸屬于一個消費組, 如果所有消費者都隸屬于同一個消費組, 那么就是點對點模式, 如果所有消費者隸屬于不同消費組就是發(fā)布/訂閱模式, 可以通過group.id來配置

消費者和消費者組.png

如圖所示某主題有4個分區(qū)p0, p1, p2, p3, 有兩個消費組A和B, A中有4個消費者, B中有2個消費者, 按照kafka默認的分配規(guī)則, 分配結果是消費者組A中每個消費者一個分區(qū), 消費者組B中每個消費者分配到2個分區(qū), 兩個消費組互不影響。

消費者組的理解

(1) 不使用消費者組的話, 每一條消息都會被分發(fā)到所有消費者(相當于每個消費是一個消費者組), 如果消費者組有10個消費者, 不使用消費者組,每一條消息都會被消費10次,消費者組為一個整體來消費主題的所有分區(qū)。
(2) 使用消費者組的話, 所有消費者組中的消費者是一個整體, 每條消息只被消費一次。
(3) 在消費者組中可以有一個或者多個消費者實例, 這些消費者共享一個公共的groupid, groupid是一個字符串,用來唯一標志一個消費者組,組內的所有消費者協(xié)調在一起來消費訂閱主題的所有分區(qū)。
(4) 同一個topic下的某個分區(qū)只能被消費者組中的一個消費者消費(消費者可以少于主題的分區(qū),一個消費者可以消費一個主題的多個分區(qū),但是如果主題分區(qū)比消費者組中的消費者少,一個主題也只會發(fā)給一個消費者不會多發(fā),此時多出來的消費者消費不到任何消息), 不同消費者組中的消費者可以消費相同的分區(qū)。
(5) 如果消費者組當中消費者的數(shù)量超過了訂閱主題分區(qū)的數(shù)量,那么多余的消費者就會被閑置,不會受到任何消息
(6) 一個消費者組的一個消費者,可以消費一個topic下的多個分區(qū)(消費者比分區(qū)少)
(7) 同一個topic下的某個分區(qū),可以被多個消費者組,消費者消息

重平衡:

新的消費者加入消費組,它會消費一個或多個分區(qū),而這些分區(qū)之前是由其他消費者負責的;另外,當消費者離開消費組(比如重啟、宕機等)時,它所消費的分區(qū)會分配給其他分區(qū)。這種現(xiàn)象稱為重平衡(rebalance)。重平衡是Kafka一個很重要的性質

消費者組和Kafka兩種模式的關系

消息中間件有兩種模式電對點(P2P)發(fā)布訂閱模式(Pub、Sub)模式

  • 如果所有消費者都在同一個消費者組,那么所有消息都會被均勻的發(fā)送到每一個消費者,每條消息只會被其中一個消費者消費,就是點對點模式。
  • 如果所有消費者屬于不同的消費者組,那么所有的消息都會被廣播到所有消費者,即每條消息都會被所有消費者處理,就相當于發(fā)布/訂閱模式。
為什么需要消費者組
  • 消費效率更高: 如果沒有消費者組,所有分區(qū)的消息都會被廣播到每一個消費者,壓力肯定大,有了消費者組,組內成員分攤分區(qū)的壓力,提高消費性能,負載均衡。
  • 消費模式靈活: 有了消費者組可以方便的設置點對點模式(所有消費者在一個消費者組)和發(fā)布訂閱模式(每個消費者在一個不同的消費者組)
  • 便于故障容災:消費組會對其成員進行管理,一個消費者宕機后,之前分配給他的分區(qū)會重新分配給其他的消費者,實現(xiàn)消費者的故障容錯,故障自動轉移。

客戶端開發(fā)

消費邏輯需要以下幾個步驟

(1) 配置消費者客戶端參數(shù)以及創(chuàng)消費者實例
(2) 訂閱主題
(3) 拉取消費者并且消費
(4) 提交消費者位移
(5) 關閉消費者實例

代碼實現(xiàn)

import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.common.serialization.StringDeserializer;

import java.time.Duration;
import java.util.Arrays;
import java.util.Collections;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicBoolean;

public class KafkaConsumerAnalysis {
    public static final String brokerList = "192.168.61.97:9092";
    public static final String topic = "test_gp";
    public static final String group_id = "group.demo";
    public static final AtomicBoolean isRuning = new AtomicBoolean(true);

    public static Properties initConfig() {
        Properties props = new Properties();
        props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, brokerList);
        props.put("key.deserializer", StringDeserializer.class.getName());
        props.put("value.deserializer", StringDeserializer.class.getName());
        props.put("group.id", group_id);
        props.put("client.id", "consumer.client.id.demo");
        return props;
    }

    public static void main(String[] args) {
        Properties props = initConfig();
        KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(props);
        consumer.subscribe(Arrays.asList(topic));

        try {
          while (isRuning.get()) {
              ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
              for (ConsumerRecord<String, String> record: records) {
                  System.out.println("topic = " + record.topic() + ", partition = " + record.partition() +
                          ", offset = " + record.offset());
                  System.out.println("key = " + record.key() + ", value = " + record.value());
              }
          }
        } catch (Exception e) {
            System.out.println(e);
        } finally {
            consumer.close();
        }
    }
}

必要的參數(shù)

消費者客戶端有4個參數(shù)需要設置
(1) bootstrap.servers: 指定連接的kafka集群所需的broker地址清單, 格式為host1:port1,host2:port2,可以設置一個或者多個地址, 用逗號隔開, 建議設置2個以上地址
(2) group.id: 消費者隸屬的消費者組, 不能為空,這個參數(shù)需要設置成具有一定業(yè)務意義的名稱。
(3) key.deserializervalue.deserializer: 與生產(chǎn)者客戶端的序列化方式一致,與key.serializervalue.serializer保持一致。消費者從broker端獲取的消息格式是字節(jié)數(shù)組byte[],所以需要響應的反序列化操作才能還原成原有的對象格式。
其他參數(shù)有client.id這個參數(shù)如果不設置KafkaConsumer會自動生成,比如“consumer-1”。
可以使用ConsumerConfig類防止參數(shù)寫錯

public static Properties initConfig() {
        Properties props = new Properties();
        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, brokerList);
        props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
        props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
        props.put(ConsumerConfig.GROUP_ID_CONFIG, group_id);
        props.put(ConsumerConfig.CLIENT_ID_CONFIG, "consumer.client.id.demo");
        return props;
    }

消費者相對于生產(chǎn)者,除了必要的序列化參數(shù)之外,多了一個group.id參數(shù).

訂閱主題和分區(qū)

主要方法有訂閱主題集合,正則表達式訂閱主題定于指定主題的分區(qū),這三種互斥, 只能指定一種。

使用集合和正則表達式訂閱主題

一個消費者可以訂閱一個或多個主題. 使用subscribe()方法訂閱主題,可以使用集合的形式和正則表達式訂閱多個主題。以下兩種方式都可以訂閱clear_data和clear_data_01兩個主題,正則表達式.*代表后續(xù)0個或者多個任意字符。

consumer.subscribe(Arrays.asList("clear_data", "clear_data_01"));

正則表達式需要設置再平衡監(jiān)聽器ConsumerRebalanceListener

consumer.subscribe(Pattern.compile("clear_data.*"), new ConsumerRebalanceListener() {
            @Override
            public void onPartitionsRevoked(Collection<TopicPartition> collection) {

            }

            @Override
            public void onPartitionsAssigned(Collection<TopicPartition> collection) {

            }
        })

如果主要消費一個主題,可以使用Collections.singletonList()將一個元素轉化為一個集合,Collections.singletonList()返回的是不可變的集合,這個長度的集合只有1,可以減少內存空間。

consumer.subscribe(Collections.singletonList("clear_data"));

如果在腳本中多次調用了了consumer.subscribe方法,最終訂閱的是腳本最下面最新定義的主題。

訂閱指定主題的指定分區(qū)

KafkaConsumer.assign()方法實現(xiàn)了這一功能, assign接收Collection<TopicPartition>, 其中TopicPartition有2個屬性, topic和partition, 分區(qū)從0開始編號, 可以通過partitionFor()方法獲得主題的分區(qū)信息, partitionFor()接受參數(shù)topic,partitionFor可以查看的參數(shù)包括:

List<PartitionInfo> res = producer.partitionsFor("pira_clear_save_data");
        for (PartitionInfo info : res) {
            System.out.println("topic:" + info.topic());
            System.out.println("partition:" + info.partition());
            System.out.println("leader:" + info.leader());
            System.out.println("replicas Array:" + Arrays.toString(info.replicas()));
            System.out.println("ISR:" + Arrays.toString(info.inSyncReplicas()));;
            System.out.println("--------------------------");
        }

PartitionInfo跟消息無關,與主題和分區(qū)本身有關,List<PartitionInfo>顯示每個partition的信息,分別是主題名,分區(qū)號(0,1,2,...)leader副本所在位置,AR集合(所有副本集合)的位置,ISR集合所在位置。

topic:clear_data
partition:2
leader:cloudera02:9092 (id: 77 rack: null)
replicas Array:[cloudera02:9092 (id: 77 rack: null), cloudera01:9092 (id: 78 rack: null), cloudera03:9092 (id: 79 rack: null)]
ISR:[cloudera02:9092 (id: 77 rack: null), cloudera01:9092 (id: 78 rack: null), cloudera03:9092 (id: 79 rack: null)]
--------------------------
topic:clear_data
partition:1
leader:cloudera03:9092 (id: 79 rack: null)
replicas Array:[cloudera03:9092 (id: 79 rack: null), cloudera02:9092 (id: 77 rack: null), cloudera01:9092 (id: 78 rack: null)]
ISR:[cloudera03:9092 (id: 79 rack: null), cloudera02:9092 (id: 77 rack: null), cloudera01:9092 (id: 78 rack: null)]
--------------------------
topic:clear_data
partition:0
leader:cloudera01:9092 (id: 78 rack: null)
replicas Array:[cloudera01:9092 (id: 78 rack: null), cloudera03:9092 (id: 79 rack: null), cloudera02:9092 (id: 77 rack: null)]
ISR:[cloudera01:9092 (id: 78 rack: null), cloudera03:9092 (id: 79 rack: null), cloudera02:9092 (id: 77 rack: null)]
--------------------------

也可以在kafka客戶端使用describe得到分區(qū)信息

Topic:pira_clear_save_data  PartitionCount:3    ReplicationFactor:3 Configs:
    Topic: pira_clear_save_data Partition: 0    Leader: 78  Replicas: 78,79,77  Isr: 78,79,77
    Topic: pira_clear_save_data Partition: 1    Leader: 79  Replicas: 79,77,78  Isr: 79,77,78
    Topic: pira_clear_save_data Partition: 2    Leader: 77  Replicas: 77,78,79  Isr: 77,78,79

在知道主題有那些分區(qū)之后可以使用KafkaConsumer.assign()訂閱指定clear_data主題分區(qū)0的消息。

consumer.assign(Arrays.asList(new TopicPartition("clear_data", 0)));
取消訂閱

取消訂閱調用unsubscribe()方法.

consumer.unsubscribe();
consumer.subscribe(new ArrayList<String>());
consumer.assign(new ArrayList<TopicPartition>());
subscribe()和assign()比較

subscribe具有自動在均衡的功能,來實現(xiàn)消費負載均衡故障自動轉移,而assign不具備這種功能。

消息消費

  • kafka中消費是基于拉模式的, 消費者主動向服務端發(fā)起請求拉取消息。kafka中的消息消費是一個不斷輪詢的過程, 消費者要做的就是重復調用poll()方法, poll()方法返回的是所訂閱的主題上的一組消息。
  • poll方法有一個超時時間參數(shù)timeout, 用來控制poll方法的阻塞時間, 在消費者緩沖區(qū)內沒有可用數(shù)據(jù)時發(fā)生阻塞,是一個Duration類型, 可以使用ofMillis(), ofSeconds(), ofMinutes(), ofHours()等多種方法指定不同的時間單位,舊版本是一個long類型。timeout的設置取決于應用程序對響應速度的要求, 比如需要在多長時間內將控制權移交給執(zhí)行輪詢的應用線程, 可以直接將timeout為0這樣poll()就會立刻返回而不管是否已經(jīng)拉取到消息.如果應用線程唯一的工作就是從Kafka拉取并消費消息, 那么可以設置為最大值Long.MAX_VALUE。
  • 消費者消費到的每條消息類型是ConsumerRecoed, 和生產(chǎn)者的ProducerRecord對應,poll()方法返回的類型是ConsumerRecords, 表示一次拉取的所有消息集。
    關于poll的理解
    (1). poll是拉取到消息立即返回的,返回自從上次poll到現(xiàn)在的數(shù)據(jù),如果沒有消息可拉,會在poll這個方法中阻塞程序運行
    (2). poll的超時時間timeout超時時間,和間隔多長時間拉取一次數(shù)據(jù)沒有關系。
    (3). 如果kafka中沒有數(shù)據(jù),等到了超時時間強制返回空消息,相當于如果一直沒有消息,則每隔超時時間返回空消息。
    (4). poll方法內部是一個do while循環(huán),只要有數(shù)據(jù)立即返回退出循環(huán),否則一直消耗超時時間,當剩余時間為0的時候退出do while返回空消息,如果設置超時時間為0,運行一次poll后剩余時間為0立即返回。
    (5). 拉取的間隔時間和程序代碼有關,是while true中兩次調用poll的程序運行間隔時間。
    (6). 相同數(shù)據(jù)吞吐量下,while true中消息處理程序運行越快,兩次調用poll間隔越短,拉取數(shù)據(jù)的間隔越短,一批次拉取的數(shù)據(jù)越少;反之,消息處理程序執(zhí)行時間越長,兩次調用poll間隔越長,拉取數(shù)據(jù)間隔越長,一批次拉取數(shù)據(jù)越多。

ConsumerRecords提供了iterator()方法遍歷消息來消費,也可以根據(jù)分區(qū)來進行消費和根據(jù)主題來進行消費。

遍歷消費
consumer.subscribe(Collections.singletonList("clear_data"));
        ConsumerRecords<String, String> consumerRecords = consumer.poll(1000L);
        for (ConsumerRecord<String, String> record : consumerRecords) {
            JSONObject jsonObject = JSON.parseObject(record.value());
            // TODO
        }
根據(jù)分區(qū)進行消費
for (TopicPartition tp : consumerRecords.partitions()) {
            for (ConsumerRecord<String, String> record : consumerRecords.records(tp)) {
                JSONObject jsonObject = JSON.parseObject(record.value());
                // TODO
            }
        }
根據(jù)主題進行消費
consumer.subscribe(Arrays.asList("clear_data", "clear_data2"));
        ConsumerRecords<String, String> consumerRecords = consumer.poll(1000L);
        for (String topic : Arrays.asList("clear_data", "clear_data2")) {
            for (ConsumerRecord<String, String> record : consumerRecords.records(topic)) {
                JSONObject jsonObject = JSON.parseObject(record.value());
                // TODO
            }
        }

位移提交

對于kafka中的分區(qū)而言, 他的每條消息都有一個唯一的offset, 用來表示消息在分區(qū)中對應的位置.對于消費者而言它也有一個offset概念, 消費者使用offset來表示消費到分區(qū)中某個消息所在的位置.

對于消息在分區(qū)中的位置, offset作為偏移量
對于消費者消費到的位置, 將offset稱為位移
對于一條消息而言, 它的偏移量和消費者消費他的位移是對等的

  • 在每次調用poll()方法時, 它返回的是沒有被消費過的消息集, 要做到這一點就必須記錄上一次消費時的位移, 并且這個位移必須做持久化保存, 而不是單單保存在內存中, 否則消費者重啟之后就不知道之前消費的位移了.另外如果在消費者組新增了一個消費者, 那么分區(qū)會進行再均衡, 會有分區(qū)從之前的消費者分配到新的消費者, 如果不持久保存消費位移, 那新的消費者就無法知道之前的消費位移。
  • 消費位移存儲在kafka日志目錄下的__consumer_offsets中, 把消費位移持久化的動作稱為提交, 消費者在消費完消息之后需要執(zhí)行消費位移提交。__consumer_offsets可以看做是一個單獨的topic,分區(qū)數(shù)在kafka.properties的offsets.topic.num.partitions中設置。
    消費位移.png

    消費者的消費位移是x, 當消費者消費完, 提交的提交的消費位移是x+1, 表示下一條要拉取的消息位置
    kafka中默認消費位移的提交方式是自動提交, 可以在消費者客戶端的參數(shù)中配置
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, true);

這個默認提交的自動提交不是每消費一條就提交一次, 而是定時提交, 默認是每個5秒, 但是自動提交是在poll方法邏輯內完成的, 在每次poll請求之前都會檢查是否可以進行位移提交, 如果可以就會提交上一次輪詢的位移
自動位移提交可能會導致重復消費數(shù)據(jù)丟失

(1) 重復消費發(fā)生在消費者崩了, 位移未提交, 下一次重新拉取, 可以通過減小自動提交位移的時間間隔縮短重新拉去的數(shù)據(jù)大小
(2) 數(shù)據(jù)丟失發(fā)生在消費者崩了, 數(shù)據(jù)還沒有處理完, 但是這一批的位移已經(jīng)提交, 重啟消費者從下一批數(shù)據(jù)開始拉取

自動位移提交在正常情況下不會發(fā)生重復消費或者數(shù)據(jù)丟失, 但是異常無法避免, kafka提供了手動位移提交, 很多時候不是消費到信息就算完成, 而是需要將消息寫入數(shù)據(jù)庫,寫入本地緩存, 或者更加復雜的業(yè)務處理才能算消費成功, 此時在進行位移提交, kafka的手動調教方式就是為了給開發(fā)人員根據(jù)邏輯在合適的地方進行位移提交, 開啟手動提交需要修改參數(shù)。

props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);

手動提交可以細分為同步提交異步提交, 對應KafkaConsumer中的commitSync()commitAsync()。

while (isRuning.get()) {
              ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(Long.MAX_VALUE));
              for (ConsumerRecord<String, String> record: records) {
                  System.out.println("topic = " + record.topic() + ", partition = " + record.partition() +
                          ", offset = " + record.offset());
                  System.out.println("key = " + record.key() + ", value = " + record.value());
              }
              consumer.commitSync();
          }

可以將消息存入內存, 進行批量處理和批量提交

final int minBatchSize = 10;
        List<ConsumerRecord> buffer = new ArrayList<>();

        while (isRuning.get()) {
            ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(Long.MAX_VALUE));
            for (ConsumerRecord<String, String> record: records) {
                buffer.add(record);
                System.out.println(buffer.size());
            }
            if (buffer.size() >= minBatchSize) {
                System.out.println(buffer);
                consumer.commitSync();
                buffer.clear();
            }
        }

commitSync()方法會阻塞消費者線程直至位移提交完成.

控制或關閉消費

KafkaConsumer提供了對消費速度進行控制的方法, 某些情況下我們可能需要暫停謀陷分區(qū)的消費而先消費其他分區(qū), 當達到一定條件是再回復這些分區(qū)的消費. KafkaConsumer中使用pause()和resume()來暫停某些分區(qū)在拉取時返回數(shù)據(jù)給消費者客戶端 和 恢復某些分區(qū)想消費者客戶端返回數(shù)據(jù)

指定位移消費

當一個新的消費組建立的時候, 他根本沒有可以找到的消費位移, 或者新訂閱了一個新的主題, 也沒有可用的位移, 或者當__consumer_offsets主題中有關消費組的位移信息被刪除, 也找不到可用的位移
當kafka中消費者找不到位移的時候, 會根據(jù)消費者客戶端參數(shù) auto.offset.reset 的配置來決定從何處開始消費. 這個參數(shù)的默認值是latest, 表示從分區(qū)末尾開始消費. 如果設置成earliest, 那么消費者會從起始處開始消費. 如果設置成none則找不到位移直接報錯.
kafka中poll無法精確掌控消費的起始位置, auto.offset.reset也只能設置從在開始或者結尾開始消費, 如果要從特定的位移開始消費需要使用KafkaConsumer的seek()方法.
先通過assignment()方法獲取消費者所分配到的分區(qū)信息

consumer.subscribe(Arrays.asList(topic));

        consumer.poll(Duration.ofMillis(10000));
        Set<TopicPartition> assignment = consumer.assignment();
        System.out.println(assignment);
[test_gp-0]

seek方法接受的參數(shù)partition和offset, offset指定從分區(qū)的哪個位置開始消費, seek需要獲取所分配的分區(qū), 要獲取所分配的分區(qū)必須先執(zhí)行一次poll操作, 因為分區(qū)分配實在poll()調用過程中實現(xiàn)的.

consumer.subscribe(Arrays.asList(topic));

        consumer.poll(Duration.ofMillis(10000));
        Set<TopicPartition> assignment = consumer.assignment();
        System.out.println(assignment);
        List<TopicPartition> assignment2 = new ArrayList<>(assignment);

        consumer.seek(assignment2.get(0), 140);

        while (true) {
            ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
            for (ConsumerRecord record : records) {
                System.out.println(record);
            }
        }
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,431評論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,637評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,555評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,900評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,629評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,976評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,976評論 3 448
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,139評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,686評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,411評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,641評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,129評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,820評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,233評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,567評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,362評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,604評論 2 380