iOS数据库的使用(三):sqlite多线程

1. 引言

首先,sqlite支持多线程,但是是有条件的支持,也就是:

  • 同一个连接不能在多线程中使用,不同连接才可以在多线程中使用。

这个是最宏观的sqlite多线程准则。

另外,sqlite 的文件锁是粗颗粒的,也就是以数据库文件为维度加锁,涉及到5种锁状态。

5中锁状态可以使用一句话来总结:sqlite 在普通情况(非普通情况就是shared-cache+wal模式)下支持并发读取操作,但是不支持并发写入操作,且不支持并发写入读取混合操作。说白了就是只能并发读取sqlite。

再者有了sqlite3.3以后的 shared-cache 模式 + WAL 模式,这两种模式使 sqlite 支持并发写入读取混合操作,也就是写入和读取都可以并发且不影响了。

本文不涉及 SHARED-CACHE + WAL 模式,只讲sqlite中的多线程及其意义和使用;

2. sqlite3中的三种线程模式

  • Single-thread(单线程)

编译时设置 -DSQLITE_THREADSAFE 值为0,所有的互斥锁都被禁止。

这种模式在极度要求速度的情况下被建议使用。因为没有加锁,所以在多线程中使用时是不安全的。该模式下性能最好,在性能优先的模式下选择。此时,一个数据库只能在一个线程使用,即使是多个线程使用多个连接来访问数据库也不行;

  • Multi-thread(多线程)

编译时设置 -DSQLITE_THREADSAFE 值为2,在部分地方加锁,部分地方禁止了互斥锁。

可以在多线程是使用多个连接,但是一个连接同时被多个线程使用时,是不安全的。

  • Serialized(串行)

编译时设置 -DSQLITE_THREADSAFE 值为1,所有的互斥锁都被开启。

这种模式无论是多个连接在多线程中使用,还是单个连接在多线程中使用,最终都被被强制成串行执行,所以是绝对线程安全的,但是性能最差,在安全性要求高的情况下选择。

官方文档:SQLITE_THREADSAFE=<0 or 1 or 2>

3. sqlite线程模式的设置

  • 编译阶段设置

通过使用编译指令配置相关的参数。例如 iOS 中的 libsqlite3.tbd 就是被编译之后的库,这个 lib 中 sqlite3 的线程模式被配置成了 2,也就是多线程模式:

iOS中的线程模式

具体的指令如下:

gcc -DSQLITE_THREADSAFE=0 shell.c sqlite3.c -ldl

其意义是:编译时设置 SQLITE_THREADSAFE 参数的值为0,编译shell.c和sqlite3.c,生成命令行执行程序。sqlite3编译设置

  • 初始化阶段设置

在调用 sqlite3_initialize() 之前使用 sqlite3_config() 函数设置。因为sqlite3_initialize()一般都被封装在了open方法中,所以这个阶段可以认为是在调用open方法之前使用sqlite3_config()来设置线程模式。

  • 运行时设置

通过sqlite3_open_v2()中的第三个参数来设置,可选值为SQLITE_OPEN_NOMUTEX(无锁即多线程模式),SQLITE_OPEN_FULLMUTEX(全锁即串行模式)

4. 编译时期设置为单线程

这里需要注意:

如果编译器设置成单线程模式,即 SQLITE_THREADSAFE == 0。那么在其他时期就没办法重新启用锁的逻辑了;

至于为什么呢,可以直接看源码。除了编译时期,对于线程模式的配置有 start 和 open 两个阶段,start 阶段是通过调用 sqlite_config() 来进行设置,open 阶段是通过 open_v2() 方法传递 NOMUTEX 和 FULLMUTEX 两个参数来进行配置;

先看看 sqlite_config 的源码关键部分:

SQLITE_API int sqlite3_config(int op, ...){
  va_list ap;
  int rc = SQLITE_OK;

  switch( op ){

    /* Mutex configuration options are only available in a threadsafe
    ** compile.(config函数只在编译时确定线程安全时才起作用)
    */
#if defined(SQLITE_THREADSAFE) && SQLITE_THREADSAFE>0 
    case SQLITE_CONFIG_SINGLETHREAD: {
      /* This option sets the threading mode to Single-thread. */
      sqlite3GlobalConfig.bCoreMutex = 0;  /* Disable mutex on core */
      sqlite3GlobalConfig.bFullMutex = 0;  /* Disable mutex on connections */
      break;
    }
#endif

#if defined(SQLITE_THREADSAFE) && SQLITE_THREADSAFE>0
    case SQLITE_CONFIG_MULTITHREAD: {
      /* This option sets the threading mode to Multi-thread. */
      sqlite3GlobalConfig.bCoreMutex = 1;  /* Enable mutex on core */
      sqlite3GlobalConfig.bFullMutex = 0;  /* Disable mutex on connections */
      break;
    }
#endif

#if defined(SQLITE_THREADSAFE) && SQLITE_THREADSAFE>0 
    case SQLITE_CONFIG_SERIALIZED: {
      /* This option sets the threading mode to Serialized. */
      sqlite3GlobalConfig.bCoreMutex = 1;  /* Enable mutex on core */
      sqlite3GlobalConfig.bFullMutex = 1;  /* Enable mutex on connections */
      break;
    }
#endif
    省略......
}

如上代码,三个 switch case 的前提都是 SQLITE_THREADSAFE>0 ,即串行模式或者多线程模式下,编译器才会生成这些代码。也就是说,编一阶段将 SQLITE_THREADSAFE 设置为 0,那么这些代码都会失效,这也是为什么此时无法在 start 阶段设置线程模式的原因;

编译时期为设置为单线程模式时不可以通过 config 将线程模式设置成其他模式模式,但是如果编译时期是非单线程模式,可以通过 config 将线程模式设置回单线程模式;

从上面代码也可以看到 sqlite 对线程模式控制的核心是 bCoreMutex 和 bFullMutex,这两个东西后面再详细看看,现在只需要知道 bCoreMutex 管理内核相关的锁,即线程锁??bFullMutex 管理数据库连接相关的锁,即防止多线程访问同一个连接??????不懂。。。

再来看看 open 方法的关键代码:

if( sqlite3GlobalConfig.bCoreMutex==0 ){
    isThreadsafe = 0;
  }else if( flags & SQLITE_OPEN_NOMUTEX ){
    isThreadsafe = 0;
  }else if( flags & SQLITE_OPEN_FULLMUTEX ){
    isThreadsafe = 1;
  }else{
    isThreadsafe = sqlite3GlobalConfig.bFullMutex;
  }
    
  if( isThreadsafe
#ifdef SQLITE_ENABLE_MULTITHREADED_CHECKS
   || sqlite3GlobalConfig.bCoreMutex
#endif
  ){
      // 获取递归锁
      // 串行模式下线程绝对安全,所以这里直接对数据库连接也进行了加锁,防止多线程访问该数据库连接
      // 如果是NOMUTEX,即多线程模式,此时不会对连接加锁,所以同一个连接被多个线程访问时可能存在问题
    // 这里是创建所,还没加锁
    db->mutex = sqlite3MutexAlloc(SQLITE_MUTEX_RECURSIVE);
      
      // 加锁失败
    if( db->mutex==0 ){
      sqlite3_free(db);
      db = 0;
      goto opendb_out;
    }
      
    if( isThreadsafe==0 ){
      sqlite3MutexWarnOnContention(db->mutex);
    }
  }

// ...省略
// 这里本质是调用pthread_mutex_lock进行加锁
sqlite3_mutex_enter(db->mutex);

这里有几个重点:

  • isThreadsafe 本质是代表当前线程模式是否为串行模式!!!

即:

  • open 方法只有在串行模式下会对同一个 sqlite 连接添加线程锁;

分析:

  1. 根据 config 方法的配置(另外初始化全局config时也有体现),只有在单线程模式下,bCoreMutex==0 ,isThreadsafe = 0;
  2. open_v2 方法如果添加了 SQLITE_OPEN_NOMUTEX 标识,即为多线程模式,官方明确说明了此时不允许多个线程访问同一个连接,所以 sqlite 没有进行加锁,isThreadsafe = 0;
  3. open_v2 如果添加了 SQLITE_OPEN_FULLMUTEX 了,首先表示编译时期不是单线程模式,不然设置会无效。另外,这个 flag 表示开启串行模式,即全锁,需要保证即使多个线程访问同一个连接也是安全的。所以 isThreadsafe = 1;
  4. bFullMutex 表示连接锁,bFullMutex = 1时在 config 代码中也有体现,此时为 bCoreMutex 也为 1 ,为串行模式;

综上:

  • open 方法只有在串行模式下才会对同一个连接添加线程锁;

bCoreMutex 和 bFullMutex

首先,根据上文 config 的源码可以知道三种模式下这两个值为:

各模式下的值

不存在 bCoreMutex = 1,bFullMutex = 1的情况;

其次,在 sqlite3GlobalConfig 初始化的时候也有体现:

sqlite3GlobalConfig初始化

SQLITE_THREADSAFE 默认为 1 ,即串行模式,0 为单线程模式,2 为多线程模式;

SQLITE_THREADSAFE

所以,bCoreMutex 的初始值正好和 SQLITE_THREADSAFE 默认值相匹配;

如果编译时期将 SQLITE_THREADSAFE 设置为了 0,那么即使初始化 sqlite3GlobalConfig 时,bCoreMutex == bFullMutex == 1,根据源码,bCoreMutex 和 bFullMutex 相关的代码直接不会被编译,所以此时 bFullMutex 和 bCoreMutex 的值并没有意义,即:什么锁也不会加;

总结:

  1. bFullMutex 表示是否需要对单个连接进行加锁;
  2. bCoreMutex 核心锁????

知道了 bFullMutex 用于单个连接的线程锁,那么 bCoreMutex 是用在哪的?

全局搜一下 bCoreMutex ,果然很多结果,这也侧面说明 bCoreMutex 的使用上比 bFullMutex 的频率更高;

sqlite3_threadsafe()方法

该方法包含以下几个重点:

  • 该方法返回编译时期所设置的线程模式的值
  • 该方法返回值不受其他阶段重置线程模式的影响,也就是说即使在初始化或者runtime阶段改变了线程模式,该函数的返回值不变。

sqlite3_config方法

#define SQLITE_CONFIG_SINGLETHREAD  1  /* nil */
#define SQLITE_CONFIG_MULTITHREAD   2  /* nil */
#define SQLITE_CONFIG_SERIALIZED    3  /* nil */
#define SQLITE_CONFIG_MALLOC        4  /* sqlite3_mem_methods* */
#define SQLITE_CONFIG_GETMALLOC     5  /* sqlite3_mem_methods* */
#define SQLITE_CONFIG_SCRATCH       6  /* No longer used */
#define SQLITE_CONFIG_PAGECACHE     7  /* void*, int sz, int N */
#define SQLITE_CONFIG_HEAP          8  /* void*, int nByte, int min */
#define SQLITE_CONFIG_MEMSTATUS     9  /* boolean */
#define SQLITE_CONFIG_MUTEX        10  /* sqlite3_mutex_methods* */
#define SQLITE_CONFIG_GETMUTEX     11  /* sqlite3_mutex_methods* */
/* previously SQLITE_CONFIG_CHUNKALLOC 12 which is now unused. */ 
#define SQLITE_CONFIG_LOOKASIDE    13  /* int int */
#define SQLITE_CONFIG_PCACHE       14  /* no-op */
#define SQLITE_CONFIG_GETPCACHE    15  /* no-op */
#define SQLITE_CONFIG_LOG          16  /* xFunc, void* */
#define SQLITE_CONFIG_URI          17  /* int */
#define SQLITE_CONFIG_PCACHE2      18  /* sqlite3_pcache_methods2* */
#define SQLITE_CONFIG_GETPCACHE2   19  /* sqlite3_pcache_methods2* */
#define SQLITE_CONFIG_COVERING_INDEX_SCAN 20  /* int */
#define SQLITE_CONFIG_SQLLOG       21  /* xSqllog, void* */
#define SQLITE_CONFIG_MMAP_SIZE    22  /* sqlite3_int64, sqlite3_int64 */
#define SQLITE_CONFIG_WIN32_HEAPSIZE      23  /* int nByte */
#define SQLITE_CONFIG_PCACHE_HDRSZ        24  /* int *psz */
#define SQLITE_CONFIG_PMASZ               25  /* unsigned int szPma */
#define SQLITE_CONFIG_STMTJRNL_SPILL      26  /* int nByte */
#define SQLITE_CONFIG_SMALL_MALLOC        27  /* boolean */
#define SQLITE_CONFIG_SORTERREF_SIZE      28  /* int nByte */
#define SQLITE_CONFIG_MEMDB_MAXSIZE       29  /* sqlite3_int64 */

各个值得意义:官方文档

几个重点

  • sqlite3_config(int,...)可以设置多个值
  • sqlite3_config不是线程安全的,当该方法在执行时,确保其他线程没有调用该方法
  • 这里的线程模式的值是1、2、3,而编译阶段设置的线程模式的值为0、1、2
Muti-thread模式下的并发

根据官方文档,只要保证了多线程中不同时使用同一个connect即可,所以path使用同一个,也就意味着使用同一个数据库,但是并发中取创建新的db,也就是open的是不同的db,也就是和同一个数据库建立了多个不同的连接,代码如下
主要并发逻辑:

- (void)mutiThreadTest {
    // iOS中的sqlite3lib默认是2,也就是muti-thread。也就是说应用程序需要自己去保证不再多线程中同时使用同一个数据库连接(database-connection)。也就是说,可以通过建立多个数据库连接来实现并行访问sqlite
    dispatch_queue_t t = dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0);
    dispatch_async(t, ^{
        FMDatabase *db1 = [FMDatabase databaseWithPath:self.path];
        [self addDataFrom:0 count:1000 withDB:db1 withFlag:@"1"];

    });
    
    dispatch_async(t, ^{
        FMDatabase *db2 = [FMDatabase databaseWithPath:self.path];
        [self addDataFrom:1000 count:1000 withDB:db2 withFlag:@"2"];
    });
    
    dispatch_async(t, ^{
        FMDatabase *db3 = [FMDatabase databaseWithPath:self.path];
        [self addDataFrom:2000 count:1000 withDB:db3 withFlag:@"3"];

    });
    
    dispatch_async(t, ^{
        FMDatabase *db4 = [FMDatabase databaseWithPath:self.path];
        [self addDataFrom:3000 count:1000 withDB:db4 withFlag:@"4"];
    });
}

viewDidLoad方法:

- (void)viewDidLoad {
    [super viewDidLoad];
    [self.view addSubview:self.tableView];
    [self create];
    
    UIButton *btn = [UIButton buttonWithType:UIButtonTypeSystem];
    [btn setTitle:@"查询" forState:UIControlStateNormal];
    btn.backgroundColor = [UIColor redColor];
    [btn addTarget:self action:@selector(mutiThreadTest) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:btn];
    btn.frame = CGRectMake(100,100, 40, 40);
}

创建数据库方法:

- (void)create {
    self.path = [NSTemporaryDirectory() stringByAppendingPathComponent:@"test.db"];
    self.db = [FMDatabase databaseWithPath:self.path];
    self.queue = [FMDatabaseQueue databaseQueueWithPath:self.path];
    if ([self.db open]) {
        [self.db executeUpdate:@"CREATE table if not exists ClientTable (name text, no text, signature text,PRIMARY KEY(no));"];
        [self.db executeUpdate:@"delete from ClientTable"];
    }
    NSLog(@"%@",self.path);
}

结果:
step方法报错,错误码为5,即SQLITE_BUSY;
所以,sqlite中的线程安全意味着什么呢?

sqlite的多线程模式中的条件的具体意义

sqlite的线程安全意味着数据的安全,如果错误的使用将会导致异常,比如崩溃、数据错乱。

如官方文档中锁描述的,其本质是不要在多线程中同时使用同一个connect或者statement,这里的具体意思:
connect的代表是sqlite3对象,所以这里的维度不是以open-close为维度,而是以sqlite对象当前被哪个线程使用到为标准,也就是说可以在thread1中open,然后再thread2中step,然后在thread3中close,只要这3个步骤不是同时进行的就没有问题。但是如果是同一个sqlite对象同时在两个线程中被调用,哪怕只是sqlite3_errmsg(sqlite);这种函数,也会引发崩溃!

验证代码:

#import "ViewController.h"
#import <sqlite3.h>

#define DATABASEPATH [[NSTemporaryDirectory() stringByAppendingPathComponent:@"testSql01.db"] UTF8String]

@interface ViewController (){
    sqlite3 *sqlite;
}

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    UIButton *btnx = [UIButton buttonWithType:UIButtonTypeSystem];
    [self.view addSubview:btnx];
    [btnx setTitle:@"多线程打开" forState:UIControlStateNormal];
    btnx.frame = CGRectMake(0, 200, 80, 20);
    btnx.backgroundColor = [UIColor redColor];
    [btnx addTarget:self action:@selector(openUnSafe) forControlEvents:UIControlEventTouchUpInside];
    
    UIButton *btn = [UIButton buttonWithType:UIButtonTypeSystem];
    [self.view addSubview:btn];
    [btn setTitle:@"串行打开" forState:UIControlStateNormal];
    btn.frame = CGRectMake(0, 300, 80, 20);
    btn.backgroundColor = [UIColor redColor];
    [btn addTarget:self action:@selector(openSafe) forControlEvents:UIControlEventTouchUpInside];
    
    UIButton *btn2 = [UIButton buttonWithType:UIButtonTypeSystem];
    [self.view addSubview:btn2];
    [btn2 setTitle:@"创建" forState:UIControlStateNormal];
    btn2.frame = CGRectMake(100, 300, 80, 20);
    btn2.backgroundColor = [UIColor redColor];
    [btn2 addTarget:self action:@selector(createDB) forControlEvents:UIControlEventTouchUpInside];
    
    UIButton *btn3 = [UIButton buttonWithType:UIButtonTypeSystem];
    [self.view addSubview:btn3];
    [btn3 setTitle:@"并发插入" forState:UIControlStateNormal];
    btn3.frame = CGRectMake(200, 300, 80, 20);
    btn3.backgroundColor = [UIColor redColor];
    [btn3 addTarget:self action:@selector(asynStep) forControlEvents:UIControlEventTouchUpInside];
    
    UIButton *btn4 = [UIButton buttonWithType:UIButtonTypeSystem];
    [self.view addSubview:btn4];
    [btn4 setTitle:@"关闭" forState:UIControlStateNormal];
    btn4.frame = CGRectMake(300, 300, 80, 20);
    btn4.backgroundColor = [UIColor redColor];
    [btn4 addTarget:self action:@selector(close) forControlEvents:UIControlEventTouchUpInside];
}

- (void)asynStep {
    NSString *name = [NSString stringWithFormat:@"%d",arc4random()/20];
    dispatch_queue_t queue = dispatch_queue_create([name UTF8String],  DISPATCH_QUEUE_CONCURRENT);
    
    // 并发操作
    dispatch_async(queue, ^{
        [self testMethod];
    });
    
    dispatch_async(queue, ^{
        [self testMethod];
    });
}

- (void)openSafe {
    int openFlage = sqlite3_open_v2(DATABASEPATH, &sqlite, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX, NULL);
    NSLog(@"path:%@",[NSString stringWithUTF8String:DATABASEPATH]);

    if (openFlage != SQLITE_OK) {
        sqlite3_close(sqlite);
        NSLog(@"数据库打开失败!");
        return;
    }
    NSLog(@"数据库打开成功!");

}

- (void)openUnSafe {
    int openFlage = sqlite3_open_v2(DATABASEPATH, &sqlite, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_NOMUTEX, NULL);
    NSLog(@"path:%@",[NSString stringWithUTF8String:DATABASEPATH]);
    NSLog(@"%@",[NSThread currentThread]);

    if (openFlage != SQLITE_OK) {
        sqlite3_close(sqlite);
        NSLog(@"数据库打开失败!");
        return;
    }
    NSLog(@"数据库打开成功!");
    
}

- (void)createDB {
    NSLog(@"%@",[NSThread currentThread]);

    NSString *deleteSql = @"delete from ClientTable";
    char *err = 0;
    int deleteFlag = sqlite3_exec(sqlite, [deleteSql UTF8String], NULL, NULL, &err);
    if (deleteFlag != SQLITE_OK) {
        NSLog(@"数据库表删除失败!");
        NSLog(@"%s",err);
    }
    
    NSString *sql = @"CREATE table if not exists ClientTable (name text, no text, signature text,PRIMARY KEY(no));delete from ClientTable";
    
    int createFlag = sqlite3_exec(sqlite, [sql UTF8String], NULL, NULL, NULL);
    if (createFlag != SQLITE_OK) {
        NSLog(@"数据库表创建失败!");
    }
    NSLog(@"数据库表创建成功!");
    sqlite3_free(err);
}


- (BOOL)testMethod {
    NSLog(@"%@",[NSThread currentThread]);
    
    // 这里即使只是调用sqlite3_errmsg方法,如果是多线程中同时使用,也会引起崩溃
    sqlite3_errmsg(sqlite);
    return YES;
}

- (void)close {
    if (sqlite3_close(sqlite) == SQLITE_OK) {
        NSLog(@"数据库关闭成功");
    }
}

@end

结果:


resutl

GIF:


result

解释:
1、第一次点击并未崩溃,这里只是概率事件,因为sqlite3_errmsg执行的速度较快的话可能会不崩溃;

2、演示过程中,数据库在多个线程进行了操作,但是只要不是同时的,就不会崩溃;

sqlite多线程的实现

根据源代码可知,多线程模式主要是由bCoreMutex和bFullMutex来控制,具体原理和实现步骤本文不作深究。


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

推荐阅读更多精彩内容