Zookeeper技术内幕

1 重要理论

1.1 数据模型znode

01.png

zk 数据存储结构与标准的 Unix 文件系统非常相似,都是在根节点下挂很多子节点。zk 中没有引入传统文件系统中目录与文件的概念,而是使用了称为znode 的数据节点概念。znode 是 zk 中数据的最小单元,每个 znode 上都可以保存数据,同时还可以挂载子节点,形成一个树形化命名空间。

(1)节点类型

  • 持久节点:其会一直保存在 zk 中,直到将其删除为止。
  • 持久顺序节点:
  • 临时节点:其与创建它的会话是绑定的,会话消失,临时节点消失
  • 临时顺序节点

(2)节点状态

  • cZxid:Created Zxid,表示当前 znode 被创建时的事务 ID
  • ctime:Created Time,表示当前 znode 被创建的时间
  • mZxid:Modified Zxid,表示当前 znode 最后一次被修改时的事务 ID
  • mtime:Modified Time,表示当前 znode 最后一次被修改时的时间
  • pZxid:表示当前 znode 的子节点列表最后一次被修改时的事务 ID。注意,只能是其子节点列表变更了才会引起 pZxid 的变更,子节点内容的修改不会影响 pZxid。
  • cversion:Children Version,表示子节点的版本号。该版本号用于充当乐观锁。
  • dataVersion:表示当前 znode 数据的版本号。该版本号用于充当乐观锁。
  • aclVersion:表示当前 znode 的权限 ACL 的版本号。该版本号用于充当乐观锁。
  • ephemeralOwner:若当前 znode 是持久节点,则其值为 0;若为临时节点,则其值为创建该节点的会话的 SessionID。当会话消失后,会根据 SessionID 来查找与该会话相关的临时节点进行删除。
  • dataLength:当前 znode 中存放的数据的长度。
  • numChildren:当前 znode 所包含的子节点的个数。

1.2 ACL

(1)ACL 简介

ACL 全称为 Access Control List(访问控制列表),是一种细粒度的权限管理策略,可以针对任意用户与组进行细粒度的权限控制。zk 利用 ACL 控制znode 节点的访问权限,如节点数据读写、节点创建、节点删除、读取子节点列表、设置节点权限等。
扩展知识:UGO(User,Group,Other),是粗粒度权限管理策略。

(2)zk 的 ACL 维度

Unix/Linux 系统的 ACL 分为两个维度:组与权限。而 Zookeeper 的 ACL 分为三个维度: 授权策略 scheme、授权对象 id、用户权限 permission。
扩展知识:大多数 Unix 已经支持 ACL,Linux 从 2.6 版本开始也支持 ACL 了

  • Unix/Linux 中的 ACL:子目录/子文件默认继承父目录的 ACL
  • zk 中的 ACL:子 znode 不会继承父 znode 的 ACL

A、授权策略 scheme

授权策略用于确定权限验证过程中使用的检验策略(简单地说就是,通过什么来验证权限,即一个用户要访问某个 znode,如何验证其身份),在 zk 中最常用的有四种策略。

  • IP
  • digest:使用用户名与密码进行验证
  • world:不验证
  • super:

B、 授权对象 id

授权对象指的是权限赋予的用户。不同的授权策略具有不同类型的授权对象。下面是各个授权模式对应的授权对象 id。

  • ip
  • digest
  • world:anyone
  • Super

C、 权限 Permission

权限指的是通过验证的用户可以对znode 执行的操作。共有五种权限,不过 zk 支持自定义权限。

  • c:create,允许授权对象在当前节点下创建子节点
  • d:delete
  • r:read
  • w:write
  • a:acl

1.3 Watcher 机制

zk 通过 Watcher 机制实现了发布/订阅模式。

(1)watcher 工作原理

02.png

(2)watcher 事件

对于同一个事件类型,在不同的通知状态中代表的含义是不同的。


屏幕截图 2021-03-16 134351.png

(3)watcher 特性

zk 的watcher 机制具有以下几个特性。

  • 一次性:zk 的watcher 机制不适合监听变化非常频繁的场景
  • 串行性:
  • 轻量级:

1.4 会话

会话是 zk 中最重要的概念之一,客户端与服务端之间的任何交互操作都与会话相关。
ZooKeeper 客户端启动时,首先会与 zk 服务器建立一个 TCP 长连接。连接一旦建立,客户端会话的生命周期也就开始了。

(1)会话状态

常见的会话状态有三种:

A、CONNECTING(重要)

连接中。客户端要创建连接,首先会在客户端创建一个zk 对象。客户端会采用轮询方式逐个获取服务器列表中的 zk 的 IP 进行连接尝试,直到连接成功。注意,在轮询之前,首先会将服务器列表打散,然后再进行轮询。

B、 CONNECTED

已连接。

C、 CLOSED

已关闭。若出现会话超时、权限验证失败或客户端主动退出等情况,客户端状态就变为了 CLOSED,注意,此时客户端的 zk 对象就消失了。

(2)会话连接超时管理

当客户端向 zk 发出连接请求后,是如何知道是否连接成功的呢?当 zk 接收到某客户端会话连接后,其会向该客户端发送连接成功 ACK。当客户端接收到 ACK 后,就知道自己已经与 zk 建立了连接。
若 zk 没有收到连接请求,或客户端没有收到 zk 发送的 ACK 怎么办呢?客户端就需要进行等待,直到发生会话连接超时。然后再进行下一次连接尝试。当然,尝试一直连接不上怎么办?这就依赖于连接时设置的超时重试策略了。
会话连接超时是由客户端维护的。

(3)会话空闲超时管理(重要)

zk 为每一个客户端都维护着空闲超时管理。一旦空闲超时,服务端就会认为该客户端已丢失,其会将该会话的 SessionId 从服务端清除。这也就是为什么客户端在空闲时需要定时向服务端发送心跳,就是为了维护这个会话长连接的。服务器是通过空闲超时管理来判断会话是否发生中断的。
会话空闲超时管理是由服务端维护的。其采用了一种特殊的方式——分桶策略。

A、基本概念

分桶策略是指,将空闲超时时间相近的会话放到同一个桶中来进行管理,以减少管理的复杂度。在检查超时时,只需要检查桶中剩下的会话即可,因为在该桶的时间范围内没有超时的会话已经被移出了桶,而桶中存在的会话就是超时的会话。
zk 对于会话空闲的超时管理并非是精确的管理,即并非是一超时马上就执行相关的超时操作。


03.png

B、 分桶依据

现要在计算当前的会话需要存放到哪个会话桶中进行管理。分桶的计算依据为:

ExpirationTime= CurrentTime + SessionTimeout
BucketTime = (ExpirationTime/ExpirationInterval + 1) * ExpirationInterval

从以上公式可知,一个桶的大小为 ExpirationInterval 时间。只要 ExpirationTime 落入到同一个桶中,系统就会对其中的会话超时进行统一管理。

(4)会话连接事件

客户端与服务端的长连接失效后,客户端将进行重连。在重连过程中客户端会产生三种会话连接事件:

  • 连接丢失
  • 会话转移
  • 会话失效

2 客户端命令

2.1 启动客户端

(1)连接本机 zk 服务器

04.png

(2)连接其它 zk 服务器

05.png

2.2 查看子节点-ls

查看根节点及/brokers 节点下所包含的所有子节点列表。


06.png

2.3 创建节点-create

(1)创建永久节点

创建一个名称为 china 的znode,其值为 999。


07.png

(2)创建顺序节点

在/china 节点下创建了顺序子节点 beijing、shanghai、guangzhou,它们的数据内容分别为 bj、sh、gz。


08.png

(3)创建临时节点

临时节点与持久节点的区别,在后面 get 命令中可以看到。


09.png

(4)创建临时顺序节点

10.png

2.4 获取节点信息-get

(1)获取持久节点数据

11.png

(2)获取顺序节点信息

12.png

(3)获取临时节点信息

13.png

2.5 更新节点数据内容-set

更新前:


14.png

更新:


15.png

16.png

2.6 删除节点-delete

17.png

若要删除具有子节点的节点,会报错。


18.png

2.7 ACL 操作

(1)查看权限-getAcl

19.png

(2)设置权限

下面的命令是,首先增加了一个认证用户 zs,密码为 123,然后为/china 节点指定只有
zs 用户才可访问该节点,而访问权限为所有权限。


20.png

3 可视化客户端

zk 常见的可视化客户端有两个:ZooView 与 ZooInspector。

3.1 ZooView

解压后直接双击运行 startup.bat 即可。


21.png

3.2 ZooInspector

在解压目录的 build 目录下进入 cmd 窗口,然后通过 jar 命令运行下面的 jar 包。


22.png

4 ZKClient 客户端

4.1 简介

ZkClient 是一个开源客户端,在 Zookeeper 原生API 接口的基础上进行了包装,更便于开发人员使用。内部实现了 Session 超时重连,Watcher 反复注册等功能。像 dubbo 等框架对其也进行了集成使用。

4.2 API 介绍

以下 API 方法均是 ZkClient 类中的方法。

(1)创建会话

ZkClient 中提供了九个构造器用于创建会话。


23.png

查看这些方法的源码可以看到具体的参数名称,这些参数的意义为:

参数名 意义
zkServers 指定zk 服务器列表,由英文状态逗号分开的 host:port 字符串组成
connectionTimeout 设置连接创建超时时间,单位毫秒。在此时间内无法创建与zk 的连接,则直接放弃连接,并抛出异常
sessionTimeout 设置会话超时时间,单位毫秒
zkSerializer 为会话指定序列化器。zk 节点内容仅支持字节数组(byte[])类型, 且 zk 不负责序列化。在创建 zkClient 时需要指定所要使用的序列化器,例如 Hessian 或Kryo。默认使用 Java 自带的序列化方式进行对象的序列化。当为会话指定了序列化器后,客户端在进行读写操作时就会自动进行序列化与反序列化。
connection IZkConnection 接口对象,是对zk 原生 API 的最直接包装,是和zk 最直接的交互层,包含了增删改查等一系列方法。该接口最常用的实现类是 zkClient 默认的实现类 ZkConnection,其可以完成绝大部分的业务需求。
operationRetryTimeout 设置重试超时时间,单位毫秒

(2)创建节点

ZkClient 中提供了 15 个方法用于创建节点。


24.png

查看这些方法的源码可以看到具体的参数名称,这些参数的意义为:

参数名 意义
path 要创建的节点完整路径
data 节点的初始数据内容,可以传入 Object 类型及 null。zk 原生API中只允许向节点传入 byte[]数据作为数据内容,但 zkClient 中具有自定义序列化器,所以可以传入各种类型对象。
mode 节点类型,CreateMode 枚举常量,常用的有四种类型。
PERSISTENT:持久型
PERSISTENT_SEQUENTIAL:持久顺序型
EPHEMERAL:临时型
EPHEMERAL_SEQUENTIAL:临时顺序型
acl 节点的 ACL 策略
callback 回调接口
context 执行回调时可以使用的上下文对象
createParents 是否级递归创建节点。zk 原生 API 中要创建的节点路径必须存在, 即要创建子节点,父节点必须存在。但zkClient 解决了这个问题, 可以做递归节点创建。没有父节点,可以先自动创建了父节点,然后再在其下创建子节点。

(3)删除节点

ZkClient 中提供了 3 个方法用于创建节点。


25.png

查看这些方法的源码可以看到具体的参数名称,这些参数的意义为:

参数名 意义
path 要删除的节点的完整路径
version 要删除的节点中包含的数据版本

(4)更新数据

ZkClient 中提供了 3 个方法用于修改节点数据内容。


26.png

查看这些方法的源码可以看到具体的参数名称,这些参数的意义为:

参数名 意义
path 要更新的节点的完整路径
data 要采用的新的数据值
expectedVersion 数据更新后要采用的数据版本号

(5)检测节点是否存在

ZkClient 中提供了 2 个方法用于判断指定节点的存在性,但 public 方法就一个:只有一个参数的exists()方法。


27.png

查看这些方法的源码可以看到具体的参数名称,这些参数的意义为:

参数名 意义
path 要判断存在性节点的完整路径
watch 要判断存在性节点及其子孙节点是否具有 watcher 监听

(6)获取节点数据内容

ZkClient 中提供了 4 个方法用于获取节点数据内容,但 public 方法就三个。


28.png

查看这些方法的源码可以看到具体的参数名称,这些参数的意义为:

参数名 意义
path 要读取数据内容的节点的完整路径
watch 指定节点及其子孙节点是否具有 watcher 监听
returnNullIfPathNotExists 这是个 boolean 值。默认情况下若指定的节点不存在,则会抛出 KeeperException$NoNodeException 异常。设置该值为 true,若指定节点不存在,则直接返回 null 而不再抛出异常。
stat 指定当前节点的状态信息。不过,执行过后该 stat 值会被最新获取到的 stat 值给替换。

(7)获取子节点列表

ZkClient 中提供了 2 个方法用于获取节点的子节点列表,但 public 方法就一个:只有一个参数的 getChildren()方法。


29.png

查看这些方法的源码可以看到具体的参数名称,这些参数的意义为:

参数名 意义
path 要获取子节点列表的节点的完整路径
watch 要获取子节点列表的节点及其子孙节点是否具有 watcher 监听

(8)watcher 注册

ZkClient 采用 Listener 来实现 Watcher 监听。客户端可以通过注册相关监听器来实现对zk 服务端事件的订阅。
可以通过 subscribeXxx()方法实现 watcher 注册,即相关事件订阅;通过 unsubscribeXxx()方法取消相关事件的订阅。


30.png

查看这些方法的源码可以看到具体的参数名称,这些参数的意义为:

参数名 意义
path 要操作节点的完整路径
watch 要判断存在性节点及其子孙节点是否具有 watcher 监听
IZkChildListener 子节点数量变化监听器
IZkDataListener 数据内容变化监听器
IZkStateListener 客户端与zk 的会话连接状态变化监听器,可以监听新会话的创建、会话创建出错、连接状态改变。连接状态是系统定义好的枚举类型 Event.KeeperState 的常量

4.3 代码演示

(1)创建工程

创建一个 Maven 的 Java 工程,并导入以下依赖。


31.png

这里仅创建一个 ZkClient 的测试类即可。本例不适合使用 JUnit 测试。


32.png

(2)代码

public class ZKClientTest {
    // 指定 zk 集群 
    private static final String CLUSTER = "zkOS:2181";
    // 指定节点名称 
    private static final String PATH = "/mylog";
    public static void main(String[] args) {
        // ---------------- 创建会话 -----------
        // 创建 zkClient
        ZkClient zkClient = new ZkClient(CLUSTER);
        // 为 zkClient 指定序列化器 
        zkClient.setZkSerializer(new SerializableSerializer()); 
        // ---------------- 创建节点 -----------
        // 指定创建持久节点 
        CreateMode mode = CreateMode.PERSISTENT;
        // 指定节点数据内容 
        String data = "first log";
        // 创建节点 
        String nodeName = zkClient.create(PATH, data, mode); 
        System.out.println("新创建的节点名称为:" + nodeName);
        // ---------------- 获取数据内容 -----------
        Object readData = zkClient.readData(PATH); 
        System.out.println("节点的数据内容为:" + readData);
        // ---------------- 注册 watcher -----------
        zkClient.subscribeDataChanges(PATH, new IZkDataListener() { 
            @Override
            public void handleDataChange(String dataPath, Object data) throws Exception{
                System.out.print(" 节 点 " + dataPath); 
                System.out.println("的数据已经更新为了" + data);
            }
            @Override
            public void handleDataDeleted(String dataPath) throws Exception { 
                System.out.println(dataPath + "的数据内容被删除");
            }
        });
        // ---------------- 更新数据内容 -----------
        zkClient.writeData(PATH, "second log"); 
        String updatedData = zkClient.readData(PATH);
        System.out.println("更新过的数据内容为:" + updatedData);

        // ---------------- 删除节点 -----------
        zkClient.delete(PATH);

        // ---------------- 判断节点存在性 -----------
        boolean isExists = zkClient.exists(PATH); 
        System.out.println(PATH + "节点仍存在吗?" + isExists);
    }
}

5 Curator 客户端

5.1 简介

Curator 是Netflix 公司开源的一套 zk 客户端框架,与 ZkClient 一样,其也封装了 zk 原生API。其目前已经成为Apache 的顶级项目。同时,Curator 还提供了一套易用性、可读性更强的 Fluent 风格的客户端 API 框架。

5.2 API 介绍

这里主要以 Fluent 风格客户端 API 为主进行介绍。

(1)创建会话

A、普通 API 创建 newClient()

在 CuratorFrameworkFactory 类中提供了两个静态方法用于完成会话的创建。


33.png

查看这些方法的源码可以看到具体的参数名称,这些参数的意义为:

参数名 意义
connectString 指定zk 服务器列表,由英文状态逗号分开的 host:port 字符串组成
sessionTimeoutMs 设置会话超时时间,单位毫秒,默认 60 秒
connectionTimeoutMs 设置连接超时时间,单位毫秒,默认 15 秒
retryPolicy 重试策略,内置有四种策略,分别由以下四个类的实例指定:ExponentialBackoffRetry、RetryNTimes、RetryOneTime、RetryUntilElapsed

B、 Fluent 风格创建

34.png

(2)创建节点 create()

下面以满足各种需求的举例方式分别讲解节点创建的方法。
说明:下面所使用的 client 为前面所创建的Curator 客户端实例。

  • 创建一个节点,初始内容为空
    语句:client.create().forPath(path);
    说明:默认创建的是持久节点,数据内容为空。
  • 创建一个节点,附带初始内容
    语句:client.create().forPath(path, “mydata”.getBytes());
    说明:Curator 在指定数据内容时,只能使用 byte[]作为方法参数。
  • 创建一个临时节点,初始内容为空
    语句:client.create().withMode(CreateMode.EPHEMERAL).forPath(path);
    说明:CreateMode 为枚举类型。
  • 创建一个临时节点,并自动递归创建父节点
    语句:client.create().createingParentsIfNeeded().withMode(CreateMode.EPHEMERAL).forPath(path);
    说明:若指定的节点多级父节点均不存在,则会自动创建。

(3)删除节点delete()

  • 删除一个节点
    语句:client.delete().forPath(path);
    说明:只能将叶子节点删除,其父节点不会被删除。
  • 删除一个节点,并递归删除其所有子节点
    语句:client.delete().deletingChildrenIfNeeded().forPath(path);
    说明:该方法在使用时需谨慎。

(4)更新数据setData()

  • 设置一个节点的数据内容
    语句:client.setData().forPath(path, newData);
    说明:该方法具有返回值,返回值为 Stat 状态对象。

(5)检测节点是否存在 checkExits()

  • 设置一个节点的数据内容
    语句:Stat stat = client.checkExists().forPath(path);
    说明:该方法具有返回值,返回值为 Stat 状态对象。若 stat 为 null,说明该节点不存在,否则说明节点是存在的。

(6)获取节点数据内容getData()

  • 读取一个节点的数据内容
    语句:byte[] data = client.getDate().forPath(path);
    说明:其返回值为byte[]数组。

(7)获取子节点列表 getChildren()

  • 读取一个节点的所有子节点列表
    语句:List<String> childrenNames = client.getChildren().forPath(path);
    说明:其返回值为byte[]数组。

(8)watcher 注册 usingWatcher()

curator 中绑定 watcher 的操作有三个:checkExists()、getData()、getChildren()。这三个方法的共性是,它们都是用于获取的。这三个操作用于 watcher 注册的方法是相同的,都是usingWatcher()方法。


35.png

这两个方法中的参数 CuratorWatcher 与 Watcher 都为接口。这两个接口中均包含一个process()方法,它们的区别是,CuratorWatcher 中的 process()方法能够抛出异常,这样的话, 该异常就可以被记录到日志中。

  • 监听节点的存在性变化
Stat stat = client.checkExists().usingWatcher((CuratorWatcher) event -> { System.out.println("节点存在性发生变化");}).forPath(path);
  • 监听节点的内容变化
byte[] data = client.getData().usingWatcher((CuratorWatcher) event -> { System.out.println("节点数据内容发生变化");}).forPath(path);
  • 监听节点子节点列表变化
List<String> sons = client.getChildren().usingWatcher((CuratorWatcher) event -> { System.out.println("节点的子节点列表发生变化");}).forPath(path);

5.3 代码演示

(1)创建工程

创建一个 Maven 的 Java 工程,并导入以下依赖。


36.png

(2)代码

public class FluentTest {
    public static void main(String[] args) throws Exception {
        // ---------------- 创建会话 -----------
        // 创建重试策略对象:第 1 秒重试 1 次,最多重试 3 次 
        ExponentialBackoffRetry retryPolicy = new ExponentialBackoffRetry(1000, 3);
        // 创建客户端 
        CuratorFramework client = CuratorFrameworkFactory
                                    .builder()
                                    .connectString("zkOS:2181")
                                    .sessionTimeoutMs(15000)
                                    .connectionTimeoutMs(13000)
                                    .retryPolicy(retryPolicy)
                                    .namespace("logs")
                                    .build();
         // 开启客户端 
        client.start();

        // 指定要创建和操作的节点,注意,其是相对于/logs 节点的 
        String nodePath = "/host";

        // ---------------- 创建节点 -----------
        String nodeName = client.create().forPath(nodePath, "myhost".getBytes());
        System.out.println("新创建的节点名称为:" + nodeName);

        // ---------------- 获取数据内容并注册 watcher -----------
        byte[] data = client.getData().usingWatcher(
            (CuratorWatcher) event -> 
            {System.out.println(event.getPath() + "数据内容发生变化");}
        ).forPath(nodePath);
        System.out.println("节点的数据内容为:" + new String(data));

        // ---------------- 更新数据内容 -----------
        client.setData().forPath(nodePath, "newhost".getBytes());
        // 获取更新过的数据内容 
        byte[] newData = client.getData().forPath(nodePath); 
        System.out.println("更新过的数据内容为:" + new String(newData));

        // ---------------- 删除节点 -----------
        client.delete().forPath(nodePath);

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

推荐阅读更多精彩内容