SpringBoot集成Kafka

一、整合Kafka

1.1 引入依賴
<dependency>
    <groupId>org.springframework.kafka</groupId>
    <artifactId>spring-kafka</artifactId>
</dependency>
1.2 設置yml文件
spring:
  application:
    name: demo

  kafka:
    bootstrap-servers: 52.82.98.209:10903,52.82.98.209:10904
    producer: # producer 生產者
      retries: 0 # 重試次數
      acks: 1 # 應答級別:多少個分區副本備份完成時向生產者發送ack確認(可選0、1、all/-1)
      batch-size: 16384 # 批量大小
      buffer-memory: 33554432 # 生產端緩沖區大小
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
#      value-serializer: com.itheima.demo.config.MySerializer
      value-serializer: org.apache.kafka.common.serialization.StringSerializer

    consumer: # consumer消費者
      group-id: javagroup # 默認的消費組ID
      enable-auto-commit: true # 是否自動提交offset
      auto-commit-interval: 100  # 提交offset延時(接收到消息后多久提交offset)

      # earliest:當各分區下有已提交的offset時,從提交的offset開始消費;無提交的offset時,從頭開始消費
      # latest:當各分區下有已提交的offset時,從提交的offset開始消費;無提交的offset時,消費新產生的該分區下的數據
      # none:topic各分區都存在已提交的offset時,從offset后開始消費;只要有一個分區不存在已提交的offset,則拋出異常
      auto-offset-reset: latest
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
#      value-deserializer: com.itheima.demo.config.MyDeserializer
      value-deserializer: org.apache.kafka.common.serialization.StringDeserializer

二、Hello Kafka

2.1 簡單生產者
@RestController
public class KafkaProducer {
    @Autowired
    private KafkaTemplate<String, Object> kafkaTemplate;

    // 發送消息
    @GetMapping("/kafka/normal/{message}")
    public void sendMessage1(@PathVariable("message") String normalMessage) {
        kafkaTemplate.send("topic1", normalMessage);
    }
}
2.2 簡單消費
@Component
public class KafkaConsumer {
    // 消費監聽
    @KafkaListener(topics = {"topic1"})
    public void onMessage1(ConsumerRecord<?, ?> record){
        // 消費的哪個topic、partition的消息,打印出消息內容
        System.out.println("簡單消費:"+record.topic()+"-"+record.partition()+"-"+record.value());
    }
}

上面示例創建了一個生產者,發送消息到topic1,消費者監聽topic1消費消息。監聽器用@KafkaListener注解,topics表示監聽的topic,支持同時監聽多個,用英文逗號分隔。

三、生產者

3.1 帶回調的生產者

kafkaTemplate提供了一個回調方法addCallback,我們可以在回調方法中監控消息是否發送成功 或 失敗時做補償處理,有兩種寫法

3.1.1 方式一
@GetMapping("/kafka/callbackOne/{message}")
public void sendMessage2(@PathVariable("message") String callbackMessage) {
    kafkaTemplate.send("topic1", callbackMessage).addCallback(success -> {
        // 消息發送到的topic
        String topic = success.getRecordMetadata().topic();
        // 消息發送到的分區
        int partition = success.getRecordMetadata().partition();
        // 消息在分區內的offset
        long offset = success.getRecordMetadata().offset();
        System.out.println("發送消息成功:" + topic + "-" + partition + "-" + offset);
    }, failure -> {
        System.out.println("發送消息失敗:" + failure.getMessage());
    });
}
3.1.2 方式二
@GetMapping("/kafka/callbackTwo/{message}")
public void sendMessage3(@PathVariable("message") String callbackMessage) {
    kafkaTemplate.send("topic1", callbackMessage).addCallback(new ListenableFutureCallback<SendResult<String, Object>>() {
        @Override
        public void onFailure(Throwable ex) {
            System.out.println("發送消息失敗:"+ex.getMessage());
        }

        @Override
        public void onSuccess(SendResult<String, Object> result) {
            System.out.println("發送消息成功:" + result.getRecordMetadata().topic() + "-"
                    + result.getRecordMetadata().partition() + "-" + result.getRecordMetadata().offset());
        }
    });
}
3.2 自定義分區器

我們知道,kafka中每個topic被劃分為多個分區,那么生產者將消息發送到topic時,具體追加到哪個分區呢?這就是所謂的分區策略,Kafka 為我們提供了默認的分區策略,同時它也支持自定義分區策略。其路由機制為:

  • 若發送消息時指定了分區(即自定義分區策略),則直接將消息append到指定分區;

  • 若發送消息時未指定 patition,但指定了 key(kafka允許為每條消息設置一個key),則對key值進行hash計算,根據計算結果路由到指定分區,這種情況下可以保證同一個 Key 的所有消息都進入到相同的分區;

  • patition 和 key 都未指定,則使用kafka默認的分區策略,輪詢選出一個 patition;

我們來自定義一個分區策略,將消息發送到我們指定的partition,首先新建一個分區器類實現Partitioner接口,重寫方法,其中partition方法的返回值就表示將消息發送到幾號分區

public class CustomizePartitioner implements Partitioner {
    @Override
    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
        // 自定義分區規則(這里假設全部發到0號分區)
        // ......
        return 0;
    }

    @Override
    public void close() {

    }

    @Override
    public void configure(Map<String, ?> configs) {

    }
}

在application.propertise中配置自定義分區器,配置的值就是分區器類的全路徑名

# 自定義分區器
spring.kafka.producer.properties.partitioner.class=com.felix.kafka.producer.CustomizePartitioner
3.3 Kafka事務提交

如果在發送消息時需要創建事務,可以使用 KafkaTemplate 的 executeInTransaction 方法來聲明事務

@GetMapping("/kafka/transaction")
public void sendMessage7(){
    // 聲明事務:后面報錯消息不會發出去
    kafkaTemplate.executeInTransaction(operations -> {
        operations.send("topic1","test executeInTransaction");
        throw new RuntimeException("fail");
    });

    // 不聲明事務:后面報錯但前面消息已經發送成功了
   kafkaTemplate.send("topic1","test executeInTransaction");
   throw new RuntimeException("fail");
}
3.4 生產者發送監聽器

異步發送的消息怎么確認發送情況呢?我們可以使用注冊監聽。

@Configuration
public class KafkaListener {
    private final static Logger logger = LoggerFactory.getLogger(KafkaListener.class);

    @Autowired
    KafkaTemplate kafkaTemplate;

    //配置監聽
    @PostConstruct
    private void listener() {
        kafkaTemplate.setProducerListener(new ProducerListener<String, Object>() {
            @Override
            public void onSuccess(ProducerRecord<String, Object> producerRecord, RecordMetadata recordMetadata) {
                logger.info("ok,message={}", producerRecord.value());
            }

            @Override
            public void onError(ProducerRecord<String, Object> producerRecord, Exception exception) {
                logger.error("error!message={}", producerRecord.value());
            }
        });
    }
}

異步發送失敗的消息會被回調給注冊過的listener。

四、消費者

4.1 指定topic、partition、offset消費

前面我們在監聽消費topic1的時候,監聽的是topic1上所有的消息,如果我們想指定topic、指定partition、指定offset來消費呢?也很簡單,@KafkaListener注解已全部為我們提供。

/**
 * @Title 指定topic、partition、offset消費
 * @Description 同時監聽topic1和topic2,監聽topic1的0號分區、topic2的 "0號和1號" 分區,指向1號分區的offset初始值為8
 * @Author long.yuan
 * @Date 2020/3/22 13:38
 * @Param [record]
 * @return void
 **/
@KafkaListener(id = "consumer1",groupId = "felix-group",topicPartitions = {
        @TopicPartition(topic = "topic1", partitions = { "0" }),
        @TopicPartition(topic = "topic2", partitions = "0", partitionOffsets = @PartitionOffset(partition = "1", initialOffset = "8"))
})
public void onMessage2(ConsumerRecord<?, ?> record) {
    System.out.println("topic:"+record.topic()+"|partition:"+record.partition()+"|offset:"+record.offset()+"|value:"+record.value());
}

屬性解釋

  • id:消費者ID;

  • groupId:消費組ID;

  • topics:監聽的topic,可監聽多個;

  • topicPartitions:可配置更加詳細的監聽信息,可指定topic、parition、offset監聽。

上面onMessage2監聽的含義:監聽topic1的0號分區,同時監聽topic2的0號分區和topic2的1號分區里面offset從8開始的消息。

注意:topics和topicPartitions不能同時使用。

4.2 批量消費

設置application.prpertise開啟批量消費即可

# 設置批量消費
spring.kafka.listener.type=batch

# 批量消費每次最多消費多少條消息
spring.kafka.consumer.max-poll-records=50

接收消息時用List來接收,監聽代碼如下

@KafkaListener(id = "consumer2",groupId = "felix-group", topics = "topic1")
public void onMessage3(List<ConsumerRecord<?, ?>> records) {
    System.out.println(">>>批量消費一次,records.size()="+records.size());
    for (ConsumerRecord<?, ?> record : records) {
        System.out.println(record.value());
    }
}
4.3 ConsumerAwareListenerErrorHandler異常處理器

通過異常處理器,我們可以處理consumer在消費時發生的異常。新建一個 ConsumerAwareListenerErrorHandler 類型的異常處理方法,用@Bean注入,BeanName默認就是方法名,然后我們將這個異常處理器的BeanName放到@KafkaListener注解的errorHandler屬性里面,當監聽拋出異常的時候,則會自動調用異常處理器。

// 新建一個異常處理器,用@Bean注入
@Bean
public ConsumerAwareListenerErrorHandler consumerAwareErrorHandler() {
    return (message, exception, consumer) -> {
        System.out.println("消費異常:"+message.getPayload());
        return null;
    };
}

// 將這個異常處理器的BeanName放到@KafkaListener注解的errorHandler屬性里面
@KafkaListener(topics = {"topic1"},errorHandler = "consumerAwareErrorHandler")
public void onMessage4(ConsumerRecord<?, ?> record) throws Exception {
    throw new Exception("簡單消費-模擬異常");
}

// 批量消費也一樣,異常處理器的message.getPayload()也可以拿到各條消息的信息
@KafkaListener(topics = "topic1",errorHandler="consumerAwareErrorHandler")
public void onMessage5(List<ConsumerRecord<?, ?>> records) throws Exception {
    System.out.println("批量消費一次...");
    throw new Exception("批量消費-模擬異常");
}
4.4 消息過濾器

消息過濾器可以在消息抵達consumer之前被攔截,在實際應用中,我們可以根據自己的業務邏輯,篩選出需要的信息再交由KafkaListener處理,不需要的消息則過濾掉。配置消息過濾只需要為 監聽器工廠 配置一個RecordFilterStrategy(消息過濾策略),返回true的時候消息將會被拋棄,返回false時,消息能正常抵達監聽容器。

@Component
public class KafkaConsumer {
    @Autowired
    ConsumerFactory consumerFactory;

    // 消息過濾器
    @Bean
    public ConcurrentKafkaListenerContainerFactory filterContainerFactory() {
        ConcurrentKafkaListenerContainerFactory factory = new ConcurrentKafkaListenerContainerFactory();
        factory.setConsumerFactory(consumerFactory);
        // 被過濾的消息將被丟棄
        factory.setAckDiscarded(true);
        // 消息過濾策略
        factory.setRecordFilterStrategy(consumerRecord -> {
            if (Integer.parseInt(consumerRecord.value().toString()) % 2 == 0) {
                return false;
            }
            //返回true消息則被過濾
            return true;
        });
        return factory;
    }

    // 消息過濾監聽
    @KafkaListener(topics = {"topic1"},containerFactory = "filterContainerFactory")
    public void onMessage6(ConsumerRecord<?, ?> record) {
        System.out.println(record.value());
    }
}

上面實現了一個"過濾奇數、接收偶數"的過濾策略,我們向topic1發送0-99總共100條消息,看一下監聽器的消費情況,可以看到監聽器只消費了偶數。

4.5 消息轉發

在實際開發中,我們可能有這樣的需求,應用A從TopicA獲取到消息,經過處理后轉發到TopicB,再由應用B監聽處理消息,即一個應用處理完成后將該消息轉發至其他應用,完成消息的轉發。在SpringBoot集成Kafka實現消息的轉發也很簡單,只需要通過一個@SendTo注解,被注解方法的return值即轉發的消息內容。

/**
 * @Title 消息轉發
 * @Description 從topic1接收到的消息經過處理后轉發到topic2
 * @Author long.yuan
 * @Date 2020/3/23 22:15
 * @Param [record]
 * @return void
 **/
@KafkaListener(topics = {"topic1"})
@SendTo("topic2")
public String onMessage7(ConsumerRecord<?, ?> record) {
    return record.value()+"-forward message";
}
4.6 定時啟動、停止監聽器

默認情況下,當消費者項目啟動的時候,監聽器就開始工作,監聽消費發送到指定topic的消息,那如果我們不想讓監聽器立即工作,想讓它在我們指定的時間點開始工作,或者在我們指定的時間點停止工作,該怎么處理呢——使用KafkaListenerEndpointRegistry,下面我們就來實現:

  • 禁止監聽器自啟動;

  • 創建兩個定時任務,一個用來在指定時間點啟動定時器,另一個在指定時間點停止定時器;

新建一個定時任務類,用注解@EnableScheduling聲明,KafkaListenerEndpointRegistry 在SpringIO中已經被注冊為Bean,直接注入,設置禁止KafkaListener自啟動。

@EnableScheduling
@Component
public class CronTimer {

    /**
     * @KafkaListener注解所標注的方法并不會在IOC容器中被注冊為Bean,
     * 而是會被注冊在KafkaListenerEndpointRegistry中,
     * 而KafkaListenerEndpointRegistry在SpringIOC中已經被注冊為Bean
     **/
    @Autowired
    private KafkaListenerEndpointRegistry registry;

    @Autowired
    private ConsumerFactory consumerFactory;

    // 監聽器容器工廠(設置禁止KafkaListener自啟動)
    @Bean
    public ConcurrentKafkaListenerContainerFactory delayContainerFactory() {
        ConcurrentKafkaListenerContainerFactory container = new ConcurrentKafkaListenerContainerFactory();
        container.setConsumerFactory(consumerFactory);
        //禁止KafkaListener自啟動
        container.setAutoStartup(false);
        return container;
    }

    // 監聽器
    @KafkaListener(id="timingConsumer",topics = "topic1",containerFactory = "delayContainerFactory")
    public void onMessage1(ConsumerRecord<?, ?> record){
        System.out.println("消費成功:"+record.topic()+"-"+record.partition()+"-"+record.value());
    }

    // 定時啟動監聽器
    @Scheduled(cron = "0 42 11 * * ? ")
    public void startListener() {
        System.out.println("啟動監聽器...");
        // "timingConsumer"是@KafkaListener注解后面設置的監聽器ID,標識這個監聽器
        if (!registry.getListenerContainer("timingConsumer").isRunning()) {
            registry.getListenerContainer("timingConsumer").start();
        }
        //registry.getListenerContainer("timingConsumer").resume();
    }

    // 定時停止監聽器
    @Scheduled(cron = "0 45 11 * * ? ")
    public void shutDownListener() {
        System.out.println("關閉監聽器...");
        registry.getListenerContainer("timingConsumer").pause();
    }
}
4.7 序列化

消費者使用:KafkaConsumer.java

@Component
public class KafkaConsumer {
    private final Logger logger = LoggerFactory.getLogger(KafkaConsumer.class);

    //不指定group,默認取yml里配置的
    @KafkaListener(topics = {"test"})
    public void onMessage1(ConsumerRecord<?, ?> consumerRecord) {
        Optional<?> optional = Optional.ofNullable(consumerRecord.value());
        if (optional.isPresent()) {
            Object msg = optional.get();
            logger.info("message:{}", msg);
        }
    }
}
4.7.1 序列化詳解

前面用到的是Kafka自帶的字符串序列化器(org.apache.kafka.common.serialization.StringSerializer)。除此之外還有:ByteArray、ByteBuffer、Bytes、Double、Integer、Long等。這些序列化器都實現了接口(org.apache.kafka.common.serialization.Serializer)。基本上,可以滿足絕大多數場景。

4.7.2 自定義序列化

自己實現,實現對應的接口即可,有以下方法:

public interface Serializer<T> extends Closeable {
    default void configure(Map<String, ?> configs, Boolean isKey) {
    }
    //理論上,只實現這個即可正常運行
    byte[] serialize(String var1, T var2);
    //默認調上面的方法
    default byte[] serialize(String topic, Headers headers, T data) {
        return this.serialize(topic, data);
    }
    default void close() {
    }
}

我們來自己實現一個序列化器:MySerializer.java

public class MySerializer implements Serializer {

    @Override
    public byte[] serialize(String s, Object o) {
        String json = JSON.toJSONString(o);
        return json.getBytes();
    }
}
4.7.3 解碼

MyDeserializer.java,實現方式與編碼器幾乎一樣

public class MyDeserializer implements Deserializer {
    private final static Logger logger = LoggerFactory.getLogger(MyDeserializer.class);

    @Override
    public Object deserialize(String s, byte[] bytes) {
        try {
            String json = new String(bytes,"utf-8");
            return JSON.parse(json);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return null;
    }

}

在yaml中配置自己的編碼器、解碼器。

4.8 位移提交
4.8.1 自動提交

前面的案例中,我們設置了以下兩個選項,則kafka會按延時設置自動提交

enable-auto-commit: true # 是否自動提交offset
auto-commit-interval: 100 # 提交offset延時(接收到消息后多久提交offset,默認單位為ms)
4.8.2 手動提交

有些時候,我們需要手動控制偏移量的提交時機,比如確保消息嚴格消費后再提交,以防止丟失或重復。下面我們自己定義配置,覆蓋上面的參數,代碼參考:MyOffsetConfig.java

@Configuration
public class MyOffsetConfig {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Value("${spring.kafka.bootstrap-servers}")
    private String bootstrapServers;

    @Bean
    public KafkaListenerContainerFactory<?> manualKafkaListenerContainerFactory() {
        Map<String, Object> configProps = new HashMap<>();
        configProps.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
        configProps.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        configProps.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        // 注意這里!!!設置手動提交
        configProps.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");
        ConcurrentKafkaListenerContainerFactory<String, String> factory =
                new ConcurrentKafkaListenerContainerFactory<>();
        factory.setConsumerFactory(new DefaultKafkaConsumerFactory<>(configProps));

        // ack模式:
        //          AckMode針對ENABLE_AUTO_COMMIT_CONFIG=false時生效,有以下幾種:
        //
        //          RECORD
        //          每處理一條commit一次
        //
        //          BATCH(默認)
        //          每次poll的時候批量提交一次,頻率取決于每次poll的調用頻率
        //
        //          TIME
        //          每次間隔ackTime的時間去commit(跟auto commit interval有什么區別呢?)
        //
        //          COUNT
        //          累積達到ackCount次的ack去commit
        //
        //          COUNT_TIME
        //          ackTime或ackCount哪個條件先滿足,就commit
        //
        //          MANUAL
        //          listener負責ack,但是背后也是批量上去
        //
        //          MANUAL_IMMEDIATE
        //          listner負責ack,每調用一次,就立即commit

        factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL_IMMEDIATE);
        return factory;
    }
}

然后通過在消費端的Consumer來提交偏移量,MyOffsetConsumer:

@Component
public class MyOffsetConsumer {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @KafkaListener(topics = "test", groupId = "myoffset-group-1", containerFactory = "manualKafkaListenerContainerFactory")
    public void manualCommit(@Payload String message,
                             @Header(KafkaHeaders.RECEIVED_PARTITION_ID) int partition,
                             @Header(KafkaHeaders.RECEIVED_TOPIC) String topic,
                             Consumer consumer,
                             Acknowledgment ack) {
        logger.info("手動提交偏移量 , partition={}, msg={}", partition, message);
        // 同步提交
        consumer.commitSync();
        //異步提交
        //consumer.commitAsync();

        // ack提交也可以,會按設置的ack策略走(參考MyOffsetConfig.java里的ack模式)
        // ack.acknowledge();
    }

    @KafkaListener(topics = "test", groupId = "myoffset-group-2", containerFactory = "manualKafkaListenerContainerFactory")
    public void noCommit(@Payload String message,
                         @Header(KafkaHeaders.RECEIVED_PARTITION_ID) int partition,
                         @Header(KafkaHeaders.RECEIVED_TOPIC) String topic,
                         Consumer consumer,
                         Acknowledgment ack) {
        logger.info("忘記提交偏移量, partition={}, msg={}", partition, message);
        // 不做commit!
    }

    /**
     * 現實狀況:
     * commitSync和commitAsync組合使用
     * <p>
     * 手工提交異步 consumer.commitAsync();
     * 手工同步提交 consumer.commitSync()
     * <p>
     * commitSync()方法提交最后一個偏移量。在成功提交或碰到無怯恢復的錯誤之前,
     * commitSync()會一直重試,但是commitAsync()不會。
     * <p>
     * 一般情況下,針對偶爾出現的提交失敗,不進行重試不會有太大問題
     * 因為如果提交失敗是因為臨時問題導致的,那么后續的提交總會有成功的。
     * 但如果這是發生在關閉消費者或再均衡前的最后一次提交,就要確保能夠提交成功。否則就會造成重復消費
     * 因此,在消費者關閉前一般會組合使用commitAsync()和commitSync()。
     */
//   @KafkaListener(topics = "test", groupId = "myoffset-group-3",containerFactory = "manualKafkaListenerContainerFactory")
    public void manualOffset(@Payload String message,
                             @Header(KafkaHeaders.RECEIVED_PARTITION_ID) int partition,
                             @Header(KafkaHeaders.RECEIVED_TOPIC) String topic,
                             Consumer consumer,
                             Acknowledgment ack) {
        try {
            logger.info("同步異步搭配 , partition={}, msg={}", partition, message);
            //先異步提交
            consumer.commitAsync();
            //繼續做別的事
        } catch (Exception e) {
            System.out.println("commit failed");
        } finally {
            try {
                consumer.commitSync();
            } finally {
                consumer.close();
            }
        }

    }


    /**
     * 甚至可以手動提交,指定任意位置的偏移量
     * 不推薦日常使用!!!
     */
//    @KafkaListener(topics = "test", groupId = "myoffset-group-4",containerFactory = "manualKafkaListenerContainerFactory")
    public void offset(ConsumerRecord record, Consumer consumer) {
        logger.info("手動指定任意偏移量, partition={}, msg={}", record.partition(), record);
        Map<TopicPartition, OffsetAndMetadata> currentOffset = new HashMap<>();
        currentOffset.put(new TopicPartition(record.topic(), record.partition()),
                new OffsetAndMetadata(record.offset() + 1));
        consumer.commitSync(currentOffset);
    }

}
4.8.3 重復消費問題

如果手動提交模式被打開,一定不要忘記提交偏移量。否則會造成重復消費。

4.8.4 經驗與總結

commitSync()方法,即同步提交,會提交最后一個偏移量。在成功提交或碰到無怯恢復的錯誤之前,commitSync()會一直重試,但是commitAsync()不會。這就造成一個陷阱:
如果異步提交,針對偶爾出現的提交失敗,不進行重試不會有太大問題,因為如果提交失敗是因為臨時問題導致的,那么后續的提交總會有成功的。只要成功一次,偏移量就會提交上去。

但是,如果這是發生在關閉消費者時的最后一次提交,就要確保能夠提交成功,如果還沒提交完就停掉了進程。就會造成重復消費。因此,在消費者關閉前一般會組合使用commitAsync()和commitSync()。詳細代碼參考:MyOffsetConsumer.manualOffset()。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,978評論 2 374

推薦閱讀更多精彩內容