2024-03-27 第三届oceanbase数据库大赛决赛|赛题相关源码解析

第三届oceanbase数据库大赛决赛|赛题相关源码解析

官方答疑ppt以及官方出品的源码解析书籍和官方文档已经讲的非常好了,这篇文章主要把和此次比赛相关的知识点汇总一下

oceanbase4.0架构简析

oceanbase基本架构

总体上,OceanBase采用了使用普通服务器和数据中心网络组成的无共享(Shared-Nothing)的集群架构,集群由若干完全对等的计算机(称为节点)组成,每个节点都有其私有的物理资源(包括CPU、内存、硬盘等),并且在这些物理资源上运行着独立的存储引擎、SQL引擎、事务引擎等。集群中各个节点之间相互独立,但通过连接彼此的网络设备相互协调,共同作为一个整体完成用户的各种请求。由于节点之间的独立性,使得OceanBase具备可扩展、高可用、高性能、低成本等核心特性。


所有服务节点都支持 SQL 计算和数据存储,每个节点自主管理所服务的分区数据。整个集群只有一种数据库服务进程,无外部服务依赖,运维管理简单。对外提供统一的数据库服务,支持 ACID 事务和全局索引,对应用开发来说与单机无异。OceanBase 可以灵活的基于用户基础设施,支持同城三中心、两地三中心、三地五中心等多种架构。

比赛的代码是基于4.x版本,而源码解析书籍是基于3.x版本,4.x版本最大的一个特性是单机分布式一体化,兼顾了分布式架构的可扩展性以及单机架构的性能,当sql语句或者事务只涉及单机内的分区的时候,便没有多机访问的开销,甚至性能比一些单机数据库更好,毕竟ob做了极致的优化。

此处仅作简单介绍,sql引擎存储引擎等的架构介绍可以查询互联网上其他资料。

oceanbase概念简析

为了组织好集群中的数据库,OceanBase集群采用了多个不同粒度的逻辑概念。下面对这些ob的概念术语进行讲解,好对ob有一个基本的认识:

分区(Partition)

OceanBase中数据分布的基本单元是分区,这里的分区和数据库领域常用的分区概念相同,即表中一部分元组所构成的集合。分区也是OceanBase中数据的物理存储单位,每一个分区都有自己的标识和存储设置。OceanBase支持多种分区策略:①范围(Range)分区;②Hash分区和Key分区;③列表(List)分区;④组合分区。

副本(Replica)

为了提高可靠性以及并行性,OceanBase数据库中会以分区为单位建立副本并分散在整个集群中,同一个分区的多个副本一起组成一个Paxos复制组。其中每时每刻有一个副本作为主副本(Leader),所有对该分区的写请求都会在主副本上进行,其他副本通过Paxos协议复制主副本上的日志来保持同步,这些副本被称为Follower。

Observer

OBServer可以视为“逻辑”服务器,一台物理服务器上可以部署一个或者多个OBServer。每台OBServer都包含SQL引擎、事务引擎和存储引擎,并管理多个数据分区,用户的SQL查询经过SQL引擎解析优化后转化为事务引擎和存储引擎的内部调用,最终作用在OBServer上的数据分区上。在OceanBase内部,OBServer由其所在物理服务器的IP地址和OBServer的服务端口唯一标识。

集群中的每一台OBServer都能接收客户端(例如obclient)的直接连接。OBServer都能感知到集群中其他OBServer的存在以及它们各自的身份(例如分区的Leader等),且OBServer都有完整的SQL、存储、事务引擎,因此每一台OBServer都可以通过协调其他OBServer共同完成收到的客户端SQL请求。

在OceanBase集群的前端会配置一个由OBProxy组成的转发层和一个负载均衡层,通过这两者配合将客户端的请求路由到最合适的OBServer,从而实现全集群的负载均衡。

1)如果收到的是查询等DML请求,那么收到请求的OBServer会完成SQL的解析、执行计划确定等动作,在执行数据操作时通过OBServer间的RPC调用让目标数据(分区)所在的OBServer协助完成数据的操作,然后将结果汇聚到接收请求的OBServer上,最后由它返回给客户端。

2)如果收到的是DDL请求,则收到请求的OBServer会将请求最终交给RootService所在的OBServer,由它调用模式服务组件提供的功能完成DDL操作。

RootService

总控服务,负责整个集群的资源调度、资源分配、数据分布信息管理以及模式服务等功能。

在集群中会有若干可用区(通常是三个)提供总控服务(RootService),整个集群中的多个总控服务呈现为一主多备的配置:集群中每一时刻仅有一个总控服务(可称为活跃总控服务)生效,其他总控服务在活跃总控服务失效时会选出一个接替其工作。运行着活跃总控服务的OBServer也被称为RootServer。跨zone主备 保证核心表、系统表高可用,保证集群管理服务、元数据服务高可用。

而 RS 不是独立的进程,在4.0版本之后,RS是启动在系统日志流leader所在的observer上面的一组服务。3.x版本当中则是启动在1 号表 (__all_core_table) leader 所在 ObServer 上的一组服务。

地域(Region)

Region指一个地域或者城市,一个Region包含一个或者多个可用区,不同Region通常距离较远。通过Region的概念,OceanBase可以支持集群跨城市部署,城市之间距离通常比较远,从而支持多城市级别的容灾。

可用区/区(Zone)

Zone是可用区(Availability Zone)的简称。一个OceanBase集群,由若干个可用区组成,每个可用区又包括多台OBServer,如下图所示。为了数据安全性和高可用性,一般会把分区的多个副本分布在不同的可用区中,从而使得单个可用区故障不影响数据库服务。


日志流

日志流是一组连续递增的日志数据,记录了数据库中的所有变更操作。变更是所有数据同步的基石,从副本、主库与备库、备份数据都依赖日志中记录的变更信息。日志流代表了一批数据的集合,包括若干 Tablet 和有序的 Redo 日志流。

4.x版本将物理的变更记录聚合成了组织良好的若干日志流:一个系统日志流和多个用户日志流。故障恢复、日志归档、备库同步等均使用一套物理变更信息。

在一个租户内,一个日志流允许有多个副本,多个副本之间基于 Paxos 协议同步数据。每个副本,包括存储在磁盘上的 Tablet 的静态数据(SSTable)、存储在内存上的 Tablet 的增量数据(MemTable)、以及记录事务的日志三类主要的数据。根据存储数据种类的不同,副本有几种不同的类型,以支持不同业务在在数据安全、性能伸缩性、可用性、成本等之间的选择。


数据分区:就是将数据按照hash或者range之类的规则进行打散,每个分区都有其对应的数据存储对象,称之为分片,也叫做tablet或者partition,它具备存储数据的能力,支持在机器之间迁移,是数据均衡的最小单位。

ob3.x版本的会对数据做预分区,每个分区一个日志流,日志流个数越多,消耗的 CPU 和内存也会更多。在分布式场景下,事务的原子性、持久性会靠多条日志流来共同保障,比如分布式事务的两阶段提交、Paxos 一致性协议等,系统开销相对单机的单条日志流会增多。在大规模场景下,这个开销并不关键,但是当ob走向中小规模的企业中,如果日志流特别多,在越小规模下开销显得就会越大。

而在4.x版本,把多条日志流合并为一条日志流,极大降低了系统负载。每台机器上的每个租户只有一个日志流,这个租户上的所有数据分区都动态绑定在该日志流之上,从而避免了大量日志流导致的 overhead。另外,分区到日志流是动态绑定的,当系统增加新的服务器时,可以把分区从源端的日志流动态解绑并重新绑定到目的端的日志流,集群会自动把数据迁移到另一个日志流里面,做数据的负载均衡,整个迁移动作都由底层自动完成,对应用透明,从而实现分区动态迁移。


大赛涉及模块源码解析

bootstrap流程源码简析

客户端发起ALTER SYSTEM BOOTSTRAP SQL指令

ObBootstrapExecutor::execute发起OB_BOOTSTRAP RPC

ObService::bootstrap开始执行bootstrap任务

首先执行pre_bootstrap,完成一些前置检查/分配系统租户资源


Prepare bootstrap的关键步骤就是发起OB_CREATE_LS RPC请求创建1号日志流,然后等待1号日志流创建完成并且选主完毕之后发起OB_EXECUTE_BOOTSTRAP RPC到master rootservice处理执行execute_bootstrap


1、然后执行execute bootstrap,该流程当中,主要的函数及其作用如下表所示:


这里挑出create_all_schema函数重点介绍一下:



整个流程的时序图如下图所示:

Create tenant流程简析

创建租户的核心流程以及对应函数主要功能如下表格所示,按照执行的先后顺序:


create_tenant_user_ls流程涉及到一个状态机,可以结合下图阅读相关流程的代码:



关键组件

总控服务

(1)元数据管理

RS通过心跳机制监控集群中各个OBServer的存活状态,并同步更新系统表,以及进行异常处理。同时也通过心跳向其他OBServer传输配置变更、模式变化等多种信息。all_root_table存放所有系统表的分区信息,all_tenant_meta_table存放所有用户表的分区信息,这些信息也由RS统一管理,其他OBServer执行请求时可以通过RS服务获取这些信息来定位要操纵的数据。RS通过RPC主动获取及定时任务维护元数据的准确性,通过位置缓存(Location Cache)模块对内部其他模块及外部OBProxy提供位置信息及副本级元信息的查询服务。为了避免队列线程池模型造成多个OBServer出现循环依赖问题,为每一级系统表使用单独队列,例如all_core_table、all_root_table、普通系统表、用户表分别有各自独立队列及工作线程。

(2)集群资源管理

集群资源管理包括Leader管理、分区负载均衡、资源单元(Resource Unit)负载均衡等任务。Leader管理包括将分区组中所有分区的Leader切到一起、将Leader切到主可用区(Primary Zone)、轮转合并及隔离切主等场景。分区负载均衡是指在租户的多个资源单元内调整分区组的分布,使得单元的负载比较均衡。资源单元负载均衡是指在多个OBServer间调度资源单元,使得OBServer的负载比较均衡。

(3)版本合并管理

不同于小版本冻结(转储)由各个OBServer自行处理,大版本冻结(合并)由RS协调发起,是一个由RS和所有分区Leader组成的两阶段分布式事务。某个分区无主会导致大版本冻结失败。合并可以由业务写入(转储达到一定的次数,由全局参数控制)触发,也可以定时触发(例如每日合并,一般设置于业务低峰期)或手动触发。RS还可以控制轮转合并,从而减少合并对业务的影响。

(4)执行管理命令

RS是管理命令执行的入口,包括BOOTSTRAP命令、ALTER SYSTEM命令和其他DDL命令。BOOTSTRAP是系统的自举过程,主要用于创建系统表、初始化系统配置等。DDL是指创建表、创建索引、删除表等动作,DDL不会被优化器处理,而是作为命令直接发送到RS,DDL产生的模式变更保存于系统表并更新到内存,然后产生新的版本号通知所有在线的OBServer,OBServer再刷新获得新版本的模式。

系统表

数据库系统的正常运转离不开元数据(Meta Data),例如表的模式(结构)信息、系统中数据的统计信息、系统的运行状态信息等,OceanBase当然也不例外。OceanBase中将元数据也按照表的形式进行组织和管理,这些存放元数据的表被称为系统表(也被称为内部表)。系统表本质上也和普通的用户表一样,可以通过SQL语句进行增删改查等操作,但系统表的操作只能由特殊的通道完成。

系统租户拥有所有的系统表,在4.0版本中,引入了meta租户,每一个user租户都会有一个对应的meta租户,meta租户也拥有系统表,用于管理元数据。为了方便对元数据的查看,OceanBase还提供了一些比较复杂的只读视图,它们被称为“虚拟表”,其名称以“all_virtual”或者“tenant_virtual”为前缀。

核心系统表

在众多的系统表中,有一类“一等公民”,它们是其他系统表能够存在的前提,因此可以称为核心系统表。由于系统表本身也需要有元数据来描述其结构,因此存放表结构信息的系统表地位自然会超然于普通系统表之上。

__all_core_table记载着系统中核心系统表的元数据,这些信息是RootService启动所需的必要信息

__all_root_table记载了表的分区和副本信息

all_table中记载着所有表(all_core_table、all_root_table、all_table本身不包括在内)的表级元数据

一个表的元数据不仅仅是表自身的描述信息,还应包括表中各列的描述数据,这些数据存放在系统表all_column中,表中的每一列在all_column中都有一行

__all_database存放着租户中所有方案(Schema)的元数据,每一个方案对应着其中的一行

__all_tablegroup中存储着所有的表组(Table Group)信息,系统中每个表组对应其中一行

__all_tenant中存储着所有的租户信息,系统中每个租户对应其中一行

__all_ddl_operation中收集着所有执行过的DDL操作的信息,该表的schema_version是主键,这是因为每一次DDL操作都会导致整个集群的模式中发生或多或少的改变(例如列结构或表结构改变),为了让系统中不同时间开始的操作能使用到合适的模式信息,OceanBase每次成功执行DDL操作后都会将模式版本增加,因此可以认为每一个DDL操作都有一个唯一的模式版本(Schema Version)

系统表初始化

一个OceanBase集群第一次被启动时,需要首先进行自举操作(Bootstrap)形成初始的系统表结构并且将集群中各个服务器节点加入到集群之中,通常这一动作是由OBD发起。OBD在启动集群时会通过检查节点数据目录的clog子目录是否存在来判断是否需要进行自举动作,如果需要进行自举,则OBD会向集群发送一系列的SQL命令完成自举,也就是前文提到的bootstrap操作:


多版本模式服务

作为多节点构成的分布式数据库系统,OceanBase集群中每一个节点上的操作都需要访问模式数据,为了更好地服务各节点上的操作,OceanBase基于模式的相对稳定性设计了一套多副本的模式管理方案:各节点上都缓存有模式数据的副本,但对于模式的修改则由RootService所在的节点实施,在完成模式修改之后由RootService将新的模式版本通知其他节点,它们将会刷新各自的模式缓存。

为了便于系统中其他模块使用模式信息,OceanBase基于节点上的模式副本包装了一套模式服务来为其他模块服务。由于系统运行中会由于DDL操作导致模式版本发生变化,不同时刻开始的操作(事务)将会看到(需要)不同版本的模式信息,这套模式服务准确来说应该被称为“多版本模式服务”。

OceanBase的多版本模式服务被实现为ObMultiVersionSchemaService类,在ObServer对象初始化过程中会调用ObServer::init_schema()方法初始化一个ObMultiVersionSchemaService实例并置于ObServer实例的schema_service_属性中。


多版本模式服务为系统中其他模块提供元数据服务,其他模块可以从模式服务获得两种形态的模式:

1)完整模式(Full Schema):包含数据库对象的完整模式信息,由名为“Ob###Schema”的类表达,其中###是数据库对象的类型名,例如图4.3中的ObDatabaseSchema表示数据库(Database)的模式。

2)简单模式(Simple Schema):仅包括完整模式中的核心部分,由名为“ObSimple###Schema”的类表达。从图4.3可以看到,简单模式中仅保留了数据库对象全局性或者关键性的信息(如数据库的ID、名称),而更细致的如字符集、排序规则等信息则仅保留在完整模式中。


OceanBase仅将模式信息中最核心的部分(即简单模式)常驻在缓存中,完整版本的模式信息根据需要载入非常驻内存,当内存不足时完整模式会被自动淘汰。

多版本模式服务提供了ObSchemaGetterGuard类作为节点上的其他模块访问模式数据的入口。利用ObSchemaGetterGuard获得的数据库对象的模式信息都是形如ObDatabaseSchema的对象,它们本质上是多版本模式服务管辖的模式缓存中某个版本的缓存数据。因此,外部模块不会孤立地修改缓存中的模式,而是在通过DDL语句修改数据库对象时同步地在模式缓存中产生新版本的模式。不过,这种动作只会发生在RootService所在的节点上,其他节点上需要通过模式刷新才能获得新版本的模式。

为什么需要多版本schema


DDL可以看作mvcc的写操作,DML则可以看作mvcc的读操作

DDL服务

为了实现对模式的修改,OceanBase在多版本模式服务中提供了ObSchemaService类作为DDL命令操纵模式的接口。ObSchemaService是一个接口类,目前它仅有一个实现:ObSchemaServiceSQLImpl类。

ObSchemaServiceSQLImpl的作用是根据外部模块的调用,返回操纵相应数据库对象的SQL服务类(ObDDLSqlService的子类,如下图中的ObDatabaseSqlService)的对象,外部模块再利用SQL服务对象的方法完成DDL操作。


下图给出了CREATE DATABASE语句执行过程中涉及的DDL服务部分:主RootService所在节点收到创建数据库的RP C请求之后将会交给其create_database方法处理,其中关于模式部分的处理最终会进入多版本模式服务的ObSchemaService(实际是一个ObSchemaServiceSQLImpl实例)中,进而通过get_database_sql_service方法取得数据库对象的DDL服务对象(ObDatabaseSqlService),最后调用其insert_database方法将新建数据库的模式信息[插图]加入多版本模式服务管辖的模式缓存中。


模式缓存

为了避免反复地从持久化存储中读出系统表数据,OceanBase在多版本模式服务中设置了模式缓存。由于OceanBase的分布式数据库特性,集群中每个节点上都会有访问模式数据的需求,因此每个节点上都有自己的模式缓存。虽然多个节点上的模式缓存形成了多副本,但整个集群中只有RootService节点才能通过执行DDL语句修改模式信息,这些缓存之间实际是一主多从的关系,非RootService节点上的缓存会随着RootService节点的缓存变化而刷新,因此不会出现缓存不一致的问题。

模式缓存分为两部分,第一部分是由ObSchemaCache描述的完整模式缓存,ObSchemaCache用于管理通过ObSchemaGetterGuard产生的完整模式,通过ObSchemaGetterGuard取完整模式时会优先在ObSchemaCache中查找,如果能命中则直接从缓存中返回完整模式,否则会构造SQL从系统表中读取元组并构造所需的完整模式。在ObSchemaCache内部又分为两个组成部分:

1)sys_cache_:是一个ObHashMap类型的Hash表,顾名思义,sys_cache_中缓存着系统级的模式数据,例如核心表、系统表等的模式。sys_cache_中缓存的信息一直常驻内存,不会因为存储空间而被替换出缓存。

2)cache_:是一个KVCache类型的KV存储,它管理着非核心数据库对象的模式,或者说sys_cache_以外的所有模式数据都被缓存在cache_中。与sys_cache_不同的是,cache_中的模式数据有可能因为存储空间不足的原因被换出缓存。

在ObSchemaCache中,每一个模式都由一个ObSchemaCacheKey唯一标识,其中包括了模式类型(schema_type_)、模式ID(schema_id_)以及模式版本(schema_version_)。被缓存的对象是ObSchema,它根据模式类型可以具体化为ObTenantSchema、ObDatabase-Schema等。


模式缓存的第二大组成部分由ObSchemaMgrCache表达,ObSchemaMgrCache中缓存着ObSchemaMgr对象,相较于缓存在ObSchemaCache中的模式对象,ObSchemaMgr更像是从另一个“视角”对模式数据的组织,一个ObSchemaMgr对象可以被看成是一个特定租户在特定模式版本下的模式数据,而ObSchemaCache则是按照数据库对象来组织模式数据。ObSchemaMgr中仅缓存简单版本的模式。


ObSchemaMgrCache中对ObSchemaMgr对象的管理相对简单粗放:采用了一个ObSche-maMgrItem数组(schema_mgr_items_属性)管理ObSchemaMgr对象及其引用计数。尽管Ob-SchemaMgrCache初始化时传入的参数指定了schema_mgr_items_中缓存的ObSchemaMgr对象的最大数量,但该数组实际仍然按最大硬上限(8192个元素,由常量属性MAX_SCHEMA_SLOT_NUM定义)分配空间决定。因此ObSchemaMgrCache中缓存的ObSchemaMgr数量有限,发生缓存替换时会将引用数为零的ObSchemaMgr对象换出。ObSchemaMgrItem中的引用数记录着ObSchemaMgr被使用的次数,通过ObSchemaGetterGuard获取模式数据时若能从ObSchemaMgrCache找到对应版本的ObSchemaMgr,则会加引用计数,用完ObSchemaGetterG-uard析构时反向减少引用计数,其目的是确保使用ObSchemaGetterGuard期间对应的ObSche-maMgr不会被淘汰。

模式刷新

OceanBase数据库集群中各个节点上都有自己的模式缓存,当主RootService节点上执行DDL操作修改模式之后,其他节点上的模式缓存需要在适当的时机进行刷新。模式缓存的刷新主要分为主动刷新和被动刷新。

主动刷新

RootServer执行完DDL操作并且更新自身的模式缓存时,会产生新的模式版本号。模式版本号可以看成是一种流水号,新的模式版本号是从前一个版本号加1形成。产生新的模式版本号之后,RootServer并不采用广播的方式通知其他节点,而是等待其他节点报告心跳(续租)时随着响应信息返回给这些节点。如图4.8所示(箭头上的数字代表步骤序号),集群中的每一个节点上的OBServer都会定期向RootServer上的主RootService发送RPC请求更新租约(同时也充当心跳包),RootService中的renew_lease方法会处理续租请求。在完成对该节点的状态更新之后,renew_lease方法会向发起请求的OBServer返回一个响应包LeaseResponse,响应包中包含有RootServer上的最新模式版本号。OBServer收到租约响应包之后会由ObHeartBeatProcess的do_heartbeat_event方法处理,其中会根据租约响应包的最新模式版本号尝试调用ObServerSchemaUpdater::try_reload_schema()进行模式重载,即将刷新任务包装成一个类型为REFRESH的ObServerSchemaTask任务放入任务队列中等待处理线程异步执行,而模式的刷新最终会被路由到该节点的多版本模式服务的refresh_and_add_schema方法中。

模式的主动刷新仅为所在节点载入新版本的SchemaMgr,即简单形式的模式。而完整模式的刷新则要依赖被动刷新方式。


被动刷新

当其他模块想要获取完整模式(会指定其模式版本)时,如果所在节点模式缓存中无法找到对应版本的完整模式,会实时触发SQL从系统表构造指定版本的完整模式,并放入到当前节点的模式缓存中。严格来说,上述模式刷新方式其实并不符合“刷新”一词的语义,因为OceanBase采用的是多版本并发控制,模式的变动并不是通过“就地”(In-place)修改的方式体现,而是形成一个新版本的完整模式。因此,所谓的模式“刷新”实际是将之前没有访问过的其他版本的模式加入到模式缓存中。两种不同形式的模式中,由于简单模式的使用会更加频繁,因此对简单模式的“刷新”采用更为激进的主动刷新方式。

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