多种MySQL与Elasticsearch的数据同步解决方案

es常用来解决大数据量下mysql查询的性能问题,而他们之间的数据同步问题就很关键。mysql和es的数据同步方案网上有很多,在这里总结记录下我使用过的三种方案。

还有一个阿里云的开源软件 canal 也可以解决这个同步问题,他的原理与下边的 go-mysql-elasticsearch 很像,都是通过监控MySQL的binlog日志来实现同步的。但是我没具体使用过,就不多说它了,感兴趣的自己去搜一下这款工具。

第一种方案:代码层面实现

项目开发我使用的是Laravel框架,所以采用了 Laravel Redis 队列 + ES API 的方式来实现的数据同步。

原理:使用 Laravel Redis 队列,在代码中MySQL新增数据之后触发异步任务调用 ES 的 API,将数据同步到ES中。

这种方案的好处就是实现和维护简单,缺点就是与业务代码耦合太重

PS:如何在laravel中接入es这个就不说了,不了解的可以看之前的文章:docker安装es以及在Laravel中的接入

  1. 在es中先创建好相应的索引(这是个商城项目,以新增商品为例)

    PUT /products/
    {
      "mappings": {
        "properties": {
          "name":{
            "type": "text",
            "analyzer": "ik_smart"
          },
          "long_name":{
            "type": "text",
            "analyzer": "ik_smart"
          },
          "brand_id":{
            "type": "integer"
          },
          "category_id":{
            "type":"integer"
          },
          "shop_id":{
            "type":"integer"
          },
          "price":{
            "type":"scaled_float",
            "scaling_factor":100
          },
          "sold_count":{
            "type":"integer"
          },
          "review_count":{
            "type":"integer"
          },
          "status":{
            "type":"integer"
          },
          "create_time" : {
              "type" : "date"
          },
          "last_time" : {
              "type" : "date"
          }
        }
      }
    }
    
  2. 修改laravel队列驱动为Redis

    # 在.env文件中修改
    QUEUE_CONNECTION=redis
    # 如果要修改更多默认配置在 config/queue.php 文件中
    
  3. 在商品模型(App\Models\Product.php)中配置

    /**
    * 取出要同步到 es中的数据
    * @return array
    */
    public function toESArray()
    {
        $arr = Arr::only($this->toArray(), [
            'id',
            'name',
            'long_name',
            'brand_id',
            'category_id',
            'shop_id',
            'price',
            'sold_count',
            'review_count',
            'status',
            'create_time',
            'last_time'
        ]);
    
        return $arr;
    }
    
  4. 创建监听任务

    php artisan make:job SyncProductToES
    
  5. 编写任务中的代码

    <?php
    
    namespace App\Jobs;
    
    use App\Models\Product;
    use Illuminate\Bus\Queueable;
    use Illuminate\Contracts\Queue\ShouldQueue;
    use Illuminate\Foundation\Bus\Dispatchable;
    use Illuminate\Queue\InteractsWithQueue;
    use Illuminate\Queue\SerializesModels;
    
    class SyncProductToES implements ShouldQueue
    {
        use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
    
        protected $product;
    
        /**
         * Create a new job instance.
         *
         * @return void
         */
        public function __construct(Product $product)
        {
            $this->product = $product;
        }
    
        /**
         * Execute the job.
         *
         * @return void
         */
        public function handle()
        {
            $data = $this->product->toESArray();
            app('es')->index([
                'index' => 'products',
                'type'  => '_doc',
                'id'    => $data['id'],
                'body'  => $data,
            ]);
        }
    }
    
  6. 在需要数据同步的地方触发这个任务

    $form->saved(function (Form $form) {
        $product = $form->model();
        dispatch(new SyncProductToES($product));
    });
    
  7. 启动队列

    php artisan queue:work
    
  8. 将mysql中已有的数据导入到es中

    上述一系列操作,可以实现增量同步,在每次新增数据时都会写入es。旧数据的全量同步我这里通过创建一个 Artisan 命令来实现。

    创建命令

    php artisan make:command Elasticsearch/SyncProducts
    

    编写代码

    <?php
    
    namespace App\Console\Commands\Elasticsearch;
    
    use App\Models\Product;
    use Illuminate\Console\Command;
    
    class SyncProducts extends Command
    {
        /**
         * The name and signature of the console command.
         *
         * @var string
         */
        protected $signature = 'es:sync-products';
    
        /**
         * The console command description.
         *
         * @var string
         */
        protected $description = '将商品数据同步到 Elasticsearch';
    
        /**
         * Create a new command instance.
         *
         * @return void
         */
        public function __construct()
        {
            parent::__construct();
        }
    
        /**
         * Execute the console command.
         */
        public function handle()
        {
            // 获取 es 对象
            $es = app('es');
    
            Product::query()
                // 使用 chunkById 避免一次性加载过多数据
                ->chunkById(100, function ($products) use ($es) {
                    $this->info(sprintf('正在同步 ID 范围为 %s 至 %s 的商品', $products->first()->id, $products->last()->id));
                    // 初始化请求体
                    $req = ['body' => []];
                    // 遍历商品
                    foreach ($products as $product) {
                        // 将商品模型转为 es 所用的数组
                        $data = $product->toESArray();
    
                        $req['body'][] = [
                            'index' => [
                                '_index' => 'products',
                                '_type'  => '_doc',
                                '_id'    => $data['id'],
                            ],
                        ];
                        $req['body'][] = $data;
                    }
                    try {
                        // 使用 bulk 方法批量创建
                        $es->bulk($req);
                    } catch (\Exception $e) {
                        $this->error($e->getMessage());
                    }
                });
            $this->info('同步完成');
        }
    }
    

    测试命令

     php artisan es:sync-products
    
  9. 线上部署

    在生产环境中,一般需要安装 Horizon 队列管理工具Supervisor 进程监视器 来更好的管理队列以及提高稳定性。这两款工具的安装配置直接看laravel官方文档就好,写的很详细:https://learnku.com/docs/laravel/7.x/horizon/7514

第二种方案:使用 go-mysql-elasticsearch 工具

go-mysql-elasticsearch是一款开源的高性能的MySQL数据同步到ES的工具,由go语言开发,编译及使用非常简单。

原理:使用mysqldump获取当前MySQL的数据,然后再通过此时binlog的name和position获取增量数据,再根据binlog构建restful api写入数据到ES中。

这种方案的好处是数据同步性能非常高,而且与业务代码完全解耦;缺点是增加了开发成本,使用相对复杂,需要安装go语言的运行环境,在多表关联同步下操作比较繁琐

注意事项:(很重要,一定要看)

  1. GitHub文档中说使用的版本要求是:MySQL < 8.0 ES < 6.0

    但经过测试,我的版本是 MySQL:8.0.26,ES:7.12.1,也可以实现增量同步。只不过不能用mysqldump来同步旧数据,因为MySQL8.0之后与之前版本相比改变挺多,目前的 go-mysql-elasticsearch 版本还不支持MySQL8.0的mysqldump

  2. MySQL binlog 格式必须是ROW模式

    必须在MySQL配置文件中修改此参数,改为row:binlog_format=row

  3. 要同步的MySQL数据表必须包含主键,否则直接忽略。这是因为如果数据表没有主键,UPDATE和DELETE操作就会因为在ES中找不到对应的document而无法进行同步

  4. 在 go-mysql-elasticsearch 运行时不能更改MySQL表结构

  1. 安装 go

    官网下载地址,自行选择版本:https://golang.google.cn/dl/

    [root@VM-0-8-centos]# wget https://golang.google.cn/dl/go1.15.5.linux-amd64.tar.gz
    [root@VM-0-8-centos]# tar -C /usr/local -zxvf go1.15.5.linux-amd64.tar.gz
    

    或者centos下直接使用yum源安装

    yum install -y go
    

    配置环境变量(GOPATH 是go项目代码放置的目录)

    [root@VM-0-8-centos go]# vim /etc/profile
    
    export GOROOT=/usr/local/go
    export GOPATH=/usr/local/app/go
    export PATH=$PATH:/usr/local/go/bin
    
    [root@VM-0-8-centos go]# source /etc/profile
    

    测试,查看go版本

    [root@VM-0-8-centos]# go version
    go version go1.15.5 linux/amd64
    [root@VM-0-8-centos]#
    
  2. 安装 go-mysql-elasticsearch

    安装依赖包

    yum install -y gettext-devel openssl-devel perl-CPAN perl-devel zlib-devel
    

    安装 go-mysql-elasticsearch

    PS:因为GitHub国内时常上不去,所以这条命令如果拉取失败的话就翻个墙,去GitHub下载安装包

    go get github.com/siddontang/go-mysql-elasticsearch
    

    下载完成后会存放到上边环境变量中配置的项目地址中,进入执行 make 操作

    [root@VM-0-8-centos ~]# cd $GOPATH/src/github.com/siddontang/go-mysql-elasticsearch
    [root@VM-0-8-centos go-mysql-elasticsearch]# ls
    clear_vendor.sh  cmd  Dockerfile  elastic  etc  go.mod  go.sum  LICENSE  Makefile  README.md  river
    [root@VM-0-8-centos go-mysql-elasticsearch]# make
    

    安装完成修改配置文件,配置文件路径就是下载的这个安装包的 etc 目录下

    需要修改的地方我都加了注释,其他的配置用默认的即可

    [root@VM-0-8-centos go-mysql-elasticsearch]# vim etc/river.toml
    
    # MySQL address, user and password
    # user must have replication privilege in MySQL.
    my_addr = "127.0.0.1:3306"  # mysql地址与端口
    my_user = "root"         # mysql用户名  
    my_pass = ""             # mysql密码
    my_charset = "utf8"          # mysql字符集
    
    # Set true when elasticsearch use https
    #es_https = false
    # Elasticsearch address  
    es_addr = "127.0.0.1:9200"  # es的地址与端口 
    # Elasticsearch user and password, maybe set by shield, nginx, or x-pack
    es_user = ""                # es用户名,没有默认为空即可
    es_pass = ""             # es密码,没有默认为空即可
    
    # Path to store data, like master.info, if not set or empty,
    # we must use this to support breakpoint resume syncing. 
    # TODO: support other storage, like etcd. 
    data_dir = "./var"           # 数据存储目录
    
    # Inner Http status address
    stat_addr = "127.0.0.1:12800"
    stat_path = "/metrics"
    
    # pseudo server id like a slave 
    server_id = 1001
    
    # mysql or mariadb
    flavor = "mysql"
    
    # mysqldump execution path
    # if not set or empty, ignore mysqldump.
    mysqldump = "mysqldump"      # 如果设置为空,则不会同步mysql中现有的旧数据
    
    # if we have no privilege to use mysqldump with --master-data,
    # we must skip it.
    #skip_master_data = false
    
    # minimal items to be inserted in one bulk
    bulk_size = 128
    
    # force flush the pending requests if we don't have enough items >= bulk_size
    flush_bulk_time = "200ms"
    
    # Ignore table without primary key
    skip_no_pk_table = false
    
    # MySQL data source
    [[source]]
    schema = "test"      # 需要同步的mysql数据库
    
    # Only below tables will be synced into Elasticsearch.
    # "t_[0-9]{4}" is a wildcard table format, you can use it if you have many sub tables, like table_0000 - table_1023
    # I don't think it is necessary to sync all tables in a database.
    tables = ["t", "t_[0-9]{4}", "tfield", "tfilter"] # 需要同步的mysql数据表
    
    # Below is for special rule mapping
    
    # Very simple example
    # 
    # desc t;
    # +-------+--------------+------+-----+---------+-------+
    # | Field | Type         | Null | Key | Default | Extra |
    # +-------+--------------+------+-----+---------+-------+
    # | id    | int(11)      | NO   | PRI | NULL    |       |
    # | name  | varchar(256) | YES  |     | NULL    |       |
    # +-------+--------------+------+-----+---------+-------+
    # 
    # The table `t` will be synced to ES index `test` and type `t`.
    # 定义mysql和es同步的对应关系,有几个写几个,下边多余的可以删掉
    [[rule]]
    schema = "test"      # 需要同步的mysql数据库
    table = "t"          # 需要同步的mysql数据表
    index = "test"       # 需要同步的es索引
    type = "t"           # 需要同步的es类型,es7之后类型只有一种,只能设为 _doc
    
    # Wildcard table rule, the wildcard table must be in source tables 
    # All tables which match the wildcard format will be synced to ES index `test` and type `t`.
    # In this example, all tables must have same schema with above table `t`;
    [[rule]]
    schema = "test"
    table = "t_[0-9]{4}"
    index = "test"
    type = "t"
    
    # Simple field rule 
    #
    # desc tfield;
    # +----------+--------------+------+-----+---------+-------+
    # | Field    | Type         | Null | Key | Default | Extra |
    # +----------+--------------+------+-----+---------+-------+
    # | id       | int(11)      | NO   | PRI | NULL    |       |
    # | tags     | varchar(256) | YES  |     | NULL    |       |
    # | keywords | varchar(256) | YES  |     | NULL    |       |
    # +----------+--------------+------+-----+---------+-------+
    #
    [[rule]]
    schema = "test"
    table = "tfield"
    index = "test"
    type = "tfield"
    
    # 这个配置是定义mysql中的字段对应es中的字段,如果全都一致可以删掉这个配置
    [rule.field]
    # Map column `id` to ES field `es_id`
    id="es_id"       # 这个就是指mysql中的id字段对应es中的es_id字段,下边同理
    # Map column `tags` to ES field `es_tags` with array type 
    tags="es_tags,list"
    # Map column `keywords` to ES with array type
    keywords=",list"
    
    # Filter rule 
    #
    # desc tfilter;
    # +-------+--------------+------+-----+---------+-------+
    # | Field | Type         | Null | Key | Default | Extra |
    # +-------+--------------+------+-----+---------+-------+
    # | id    | int(11)      | NO   | PRI | NULL    |       |
    # | c1    | int(11)      | YES  |     | 0       |       |
    # | c2    | int(11)      | YES  |     | 0       |       |
    # | name  | varchar(256) | YES  |     | NULL    |       |
    # +-------+--------------+------+-----+---------+-------+
    #
    [[rule]]
    schema = "test"
    table = "tfilter"
    index = "test"
    type = "tfilter"
    
    # Only sync following columns
    filter = ["id", "name"]      # 指定mysql中哪些字段需要同步
    
    # id rule
    #
    # desc tid_[0-9]{4};
    # +----------+--------------+------+-----+---------+-------+
    # | Field    | Type         | Null | Key | Default | Extra |
    # +----------+--------------+------+-----+---------+-------+
    # | id       | int(11)      | NO   | PRI | NULL    |       |
    # | tag      | varchar(256) | YES  |     | NULL    |       |
    # | desc     | varchar(256) | YES  |     | NULL    |       |
    # +----------+--------------+------+-----+---------+-------+
    #
    [[rule]]
    schema = "test"
    table = "tid_[0-9]{4}"
    index = "test"
    type = "t"
    # The es doc's id will be `id`:`tag`
    # It is useful for merge muliple table into one type while theses tables have same PK 
    id = ["id", "tag"]
    

    再提供个本次测试使用的配置文件,去掉了所有的注释,这样看起来简洁一点

    my_addr = "172.17.0.4:3306"  
    my_user = "root"
    my_pass = "root"
    my_charset = "utf8"
    
    es_addr = "172.17.0.7:9200"
    es_user = ""
    es_pass = ""
    
    data_dir = "/docker/data"
    
    stat_addr = "127.0.0.1:12800"
    stat_path = "/metrics"
    server_id = 1001
    flavor = "mysql"
    mysqldump = ""
    bulk_size = 128
    flush_bulk_time = "200ms"
    skip_no_pk_table = false
    
    [[source]]
    schema = "lmrs"
    tables = ["lmrs_products"]
    
    [[rule]]
    schema = "lmrs"
    table = "lmrs_products"
    index = "products"
    type = "_doc"
    filter = ["id","name","long_name","brand_id","shop_id","price","sold_count","review_count","status","create_time","last_time","three_category_id"]
    
    [rule.field]
    mysql = "three_category_id"
    elastic = "category_id"
    

    启动 go-mysql-elasticsearch,输出以下信息证明成功

    [root@VM-0-8-centos go-mysql-elasticsearch]# ./bin/go-mysql-elasticsearch -config=./etc/river.toml
    [2021/08/01 13:37:06] [info] binlogsyncer.go:141 create BinlogSyncer with config {1001 mysql 127.0.0.1 3306 root   utf8mb4 false false <nil> false UTC false 0 0s 0s 0 false 0}
    [2021/08/01 13:37:06] [info] dump.go:180 skip dump, use last binlog replication pos (mysql-bin.000001, 2606) or GTID set <nil>
    [2021/08/01 13:37:06] [info] binlogsyncer.go:362 begin to sync binlog from position (mysql-bin.000001, 2606)
    [2021/08/01 13:37:06] [info] binlogsyncer.go:211 register slave for master server 127.0.0.1:3306
    [2021/08/01 13:37:06] [info] sync.go:25 start sync binlog at binlog file (mysql-bin.000001, 2606)
    [2021/08/01 13:37:06] [info] binlogsyncer.go:731 rotate to (mysql-bin.000001, 2606)
    [2021/08/01 13:37:06] [info] sync.go:71 rotate binlog to (mysql-bin.000001, 2606)
    [2021/08/01 13:37:06] [info] master.go:54 save position (mysql-bin.000001, 2606)
    
  3. 如果觉得上述两步太麻烦,可以直接使用docker来安装 go-mysql-elasticsearch,镜像中自带了go语言环境

    拉取镜像

    docker pull gozer/go-mysql-elasticsearch
    

    构建容器,其中 river.toml 配置文件与上边的内容一样

    docker run -p 12345:12345 -d --name go-mysql-es -v /docker/go-mysql-es/river.toml:/config/river.toml --privileged=true gozer/go-mysql-elasticsearch
    

第三种方案:使用 Logstash 工具

Logstash 是免费且开放的服务器端数据处理管道,能够从多个来源采集数据,转换数据,然后将数据发送到您最喜欢的“存储库”中,可与各种部署集成。 它提供了大量插件,可帮助你解析,丰富,转换和缓冲来自各种来源的数据。 如果你的数据需要 Beats 中没有的其他处理,则需要将 Logstash 添加到部署中。

这个工具不止可以用来做mysql到es的数据同步,它的应用场景还有:日志搜索器( logstash采集、处理、转发到elasticsearch存储,在kibana进行展示)、elk日志分析(elasticsearch + logstash + kibana)等。

它既可以全量同步旧数据,也可以增量同步新数据,而且对mysql和es没有版本方面的限制,只需对应版本即可

  1. 安装

    官方下载地址:https://www.elastic.co/cn/downloads/past-releases#logstash

    PS: logstash 的版本一定要和 es 保持一致,我的 es 是 7.12.1 版本,所以 logstash 也下载的 7.12.1 版本

    wget https://artifacts.elastic.co/downloads/logstash/logstash-7.12.1-linux-x86_64.tar.gz
    

    也可以直接使用docker安装,更方便

    docker pull logstash:7.12.1
    
  2. 安装两个插件

    logstash-input-jdbc:连接读取mysql中数据的插件(6.0之后的版本已经自带了,再次安装会提示报错)

    logstash-output-elasticsearch:数据输出到es的插件

    [root@localhost]# tar -C /usr/local -zxvf logstash-7.12.1-linux-x86_64.tar.gz
    [root@localhost]# cd /usr/local/logstash-7.12.1/bin
    [root@localhost bin]# ./logstash-plugin install logstash-input-jdbc
    ...
    ERROR: Installation aborted, plugin 'logstash-input-jdbc' is already provided by 'logstash-integration-jdbc'
    [root@localhost bin]# ./logstash-plugin install logstash-output-elasticsearch
    ...
    Installation successful
    [root@localhost bin]#
    
  3. 下载 jdbc 的 mysql-connection.jar 包,版本与自己的 mysql 版本保持一致

    [root@localhost logstash-7.12.1]# mkdir pipeline
    [root@localhost logstash-7.12.1]# cd pipeline/
    [root@localhost pipeline]# wget https://repo1.maven.org/maven2/mysql/mysql-connector-java/8.0.26/mysql-connector-java-8.0.26.jar
    
  4. 更改配置文件

    [root@localhost logstash-7.12.1]# vi config/logstash.yml
    # 加入以下内容,下边那个是es的地址,根据自己的情况改
    http.host: "0.0.0.0"
    xpack.monitoring.elasticsearch.hosts: ["http://172.17.0.2:9200"]
    
    [root@localhost logstash-7.12.1]# vi config/pipelines.yml
    # 加入以下内容,路径同样也是根据自己实际的来
    pipeline.id: table1
    path.config: "/usr/local/logstash-7.12.1/pipeline/logstash.config"
    
  5. 创建上边配置里的指定的配置文件 logstash.config

    vi pipeline/logstash.config

    input {
        stdin {}
        # 可以有多个jdbc,来同步不同的数据表
        jdbc {
            # 类型,区分开每个 jdbc,以便输出的时候做判断
            type => "product"
            # 注意mysql连接地址一定要用ip,不能使用localhost等
            jdbc_connection_string => "jdbc:mysql://172.17.0.4:3306/lmrs"
            jdbc_user => "root"
            jdbc_password => "root"
            # 数据库重连尝试次数
            connection_retry_attempts => "3"
            # 数据库连接校验超时时间,默认为3600s
            jdbc_validation_timeout => "3600"
            # 这个jar包就是上边下载那个,可以是绝对路径也可以是相对路径,把地址写对
            jdbc_driver_library => "/usr/local/logstash-7.12.1/pipeline/mysql-connector-java-8.0.26.jar"
            # 驱动类名
            jdbc_driver_class => "com.mysql.jdbc.Driver"
            # 开启分页,默认是 false
            jdbc_paging_enabled => "true"
            # 单次分页查询条数(默认100000,字段较多的话,可以适当调整这个数值)
            jdbc_page_size => "50000"
            # 要执行的sql,从这查出的数据就会同步到es中
            statement => "select id,`name`,long_name,brand_id,three_category_id as category_id,shop_id,price,status,sold_count,review_count,create_time,last_time from lmrs_products"
            # 执行的sql文件路径,这与上边的 statement 参数 二选一
            # statement_filepath => "/usr/local/logstash-7.12.1/pipeline/products.sql"
            # 是否将字段名转为小写,默认为true(如果具备序列化或者反序列化,建议设置为false)
            lowercase_column_names => false
            # 需要记录查询结果某字段的值时,此字段为true,否则默认tracking_colum为timestamp的值
            use_column_value => true
            # 需要记录的字段,同于增量同步,需要是数据库字段
            tracking_column => id
            # 记录字段的数据类型
            tracking_column_type => numeric
            # 上次数据存放位置
            record_last_run => true
            # 上一个sql_last_value的存放路径,必须在文件中指定字段的初始值,手动创建文件并赋予读写权限
            last_run_metadata_path => "/usr/local/logstash-7.12.1/pipeline/products.txt"
            # 是否清除last_run_metadata_path的记录,需要增量同步这个字段的值必须为false
            clean_run => false
            # 设置定时任务间隔  含义:分、时、天、月、年,全部为*默认为每分钟跑一次任务
            schedule => "* * * * *"
        }
    }
    output {
        # 判断类型
        if [type] == "product" {
            # es的配置
            elasticsearch {
                hosts => "172.17.0.2:9200"
                index => "products"
                document_type => "_doc"
                document_id => "%{id}"
            }
        }
    
        # 日志输出
        stdout {
            codec => json_lines
        }
    }
    
  6. 启动 Logstash(--config.reload.automatic 选项启用自动配置重新加载,不必在每次修改配置文件时停止并重新启动 Logstash)

    [root@localhost logstash-7.12.1]# ./bin/logstash -f pipeline/logstash.config --config.reload.automatic
    

    浏览器访问 ip:9600 可以打印出以下信息证明启动成功

    {"host":"localhost.localdomain","version":"7.12.1","http_address":"0.0.0.0:9600","id":"15320442-569b-4bfd-a0d6-4c71619bc06d","name":"localhost.localdomain","ephemeral_id":"f6868c4c-fff1-4b6a-89d9-4ca7ea469c6e","status":"green","snapshot":false,"pipeline":{"workers":4,"batch_size":125,"batch_delay":50},"build_date":"2021-04-20T19:51:54Z","build_sha":"a0a95c823ae2da19a75f44a01784665e7ad23d15","build_snapshot":false}
    

总结

go-mysql-elasticsearch 和 Logstash 工具都可以放到 Supervisor 中来管控,来提高稳定性。

这三种方案总的来说各有利弊,至于选择哪种个人认为:如果项目不是特别大,数据量增长速度也不快,对性能没太高的要求的话可以考虑第一种,因为实现简单,利于维护;如果对双方同步性能要求比较高,或者数据量很大的情况下,就考虑后两种。

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

推荐阅读更多精彩内容