Redis分布式鎖使用總結(jié)

Redis分布式鎖使用總結(jié)

前言

最近因?yàn)轫?xiàng)目需要進(jìn)行多實(shí)例的協(xié)調(diào)恃泪,使用到了分布式鎖郑兴,所以對(duì)分布式鎖的原理、使用等做了一番調(diào)查贝乎、學(xué)習(xí)情连,順便將其記錄下來(lái),供需要的同學(xué)學(xué)習(xí)交流览效。

項(xiàng)目中使用的是基于Redis的分布式鎖却舀,所以這篇文件的內(nèi)容都是是基于Redis分布式鎖虫几。

分布式鎖簡(jiǎn)介

談起編程語(yǔ)言中的鎖,開(kāi)發(fā)者應(yīng)該是相當(dāng)熟悉的挽拔,當(dāng)系統(tǒng)中存在多線(xiàn)程并且多線(xiàn)程之間存在競(jìng)態(tài)條件或者需要協(xié)作的時(shí)候辆脸,我們就會(huì)使用到鎖,如Java中的Lock螃诅、Synchronized等啡氢,但是編程語(yǔ)言中提供的鎖,基本上都只適用于在同一個(gè)機(jī)器上運(yùn)行的情況术裸,在分布式環(huán)境下并不適用倘是。

而在某些情況下,我們是需要在多個(gè)機(jī)器實(shí)例/節(jié)點(diǎn)之間進(jìn)行協(xié)作的袭艺,這個(gè)時(shí)候搀崭,就需要使用到分布式鎖了。

顧名思義猾编,分布式鎖就是應(yīng)用于在分布式環(huán)境下多個(gè)節(jié)點(diǎn)之間進(jìn)行同步或者協(xié)作的鎖

分布式鎖同普通的鎖一樣瘤睹,具有以下幾個(gè)重要特性

  • 互斥性,保證只有持有鎖的實(shí)例中的某個(gè)線(xiàn)程才能進(jìn)行操作
  • 可重入性答倡,同一個(gè)實(shí)例的同一個(gè)線(xiàn)程可以多次獲取鎖
  • 鎖超時(shí)轰传,支持超時(shí)自動(dòng)釋放鎖,避免死鎖的產(chǎn)生
  • 誰(shuí)加的鎖只能由誰(shuí)釋放

Redis分布式鎖原理

由于Redis的命令本身是原子性的瘪撇,所以绸吸,非常適合于作為分布式鎖的協(xié)調(diào)者。

一般情況下设江,為了保證鎖的釋放只能由加鎖者或者超時(shí)釋放锦茁,一般我們會(huì)將對(duì)應(yīng)鍵的值設(shè)置為一個(gè)線(xiàn)程唯一標(biāo)志,如為每個(gè)線(xiàn)程生成一個(gè)UUID叉存,只有當(dāng)線(xiàn)程的UUID與鎖的值一致時(shí)码俩,才能釋放鎖。

利用Redis來(lái)實(shí)現(xiàn)分布式的原理非常簡(jiǎn)單歼捏,加鎖的時(shí)候?yàn)槟硞€(gè)鍵設(shè)置值稿存,釋放的時(shí)候?qū)?duì)應(yīng)的鍵刪除即可。

不過(guò)在使用的時(shí)候瞳秽,有一些需要注意的地方瓣履,下面我們?cè)敿?xì)看下基于Redis不同命令來(lái)實(shí)現(xiàn)分布式鎖的操作

setnx命令

在Redis2.6之前,常用于分布式鎖的命令是:setnx key val练俐,該命令在對(duì)應(yīng)的鍵沒(méi)有值的時(shí)候設(shè)置成功袖迎,存在值的時(shí)候設(shè)置失敗,保證了同時(shí)只會(huì)有一個(gè)連接者設(shè)置成功,也即保證同時(shí)只會(huì)有一個(gè)實(shí)例的一個(gè)線(xiàn)程獲取成功燕锥。

但是該命令存在一個(gè)缺陷辜贵,不支持超時(shí)機(jī)制,所以需要額外的命令來(lái)保證能夠在超時(shí)的情況下釋放鎖归形,也就是刪除鍵托慨,可以配合expire命令來(lái)實(shí)現(xiàn)。

由于上述操作涉及到兩個(gè)命令暇榴,所以最好的方式是通過(guò)lua腳本來(lái)實(shí)現(xiàn)加鎖的操作厚棵,如下所示

# KEYS[1]是鎖的名稱(chēng),KEYS[2]是鎖的值蔼紧,KEYS[3]是鎖的超時(shí)時(shí)間
local c = redis.call('setnx', KEYS[1], KEYS[2])
if(c == 1) then
   redis.call('expire', KEYS[1], KEYS[3])
end
return c

釋放鎖的時(shí)候窟感,需要驗(yàn)證釋放鎖的是不是鎖的持有者,具體代碼如下

# KEYS[1]是鎖的名稱(chēng)歉井,KEYS[2]是鎖的值
if redis.call('get', KEYS[1]) == KEYS[2] then 
    return redis.call('del', KEYS[1]) 
else return 0 
end

set命令

從上面的setnx命令可以看到,加鎖的操作還是比較麻煩的哈误,所以哩至,在Redis2.6之后,redis的set命令進(jìn)行了增強(qiáng)蜜自,設(shè)置值的時(shí)候菩貌,同時(shí)支持設(shè)置過(guò)期時(shí)間

# nx表示不存在的時(shí)候設(shè)置,ex表示設(shè)置過(guò)期時(shí)間重荠,單位是秒
set LOCK VAL nx ex 15

可以看到箭阶,通過(guò)該命令,進(jìn)行加鎖就方便很多了

釋放鎖的操作同setnx里提到的釋放操作

Redis分布式鎖實(shí)現(xiàn)

上面我們提到的是Redis分布式鎖的實(shí)現(xiàn)原理戈鲁,不過(guò)仇参,每次需要用到鎖的時(shí)候都需要自己手動(dòng)實(shí)現(xiàn)一次,雖然代碼本身沒(méi)有多少婆殿,其實(shí)也不是很方便诈乒。

正因?yàn)槿绱耍型Χ嗟捻?xiàng)目都實(shí)現(xiàn)了分布式婆芦,并且提供了更加豐富的功能怕磨,如下面討論到的RedisLockRegistry

RedisLockRegistry

Spring-integration項(xiàng)目是Spring官方提供了集成各種工具的項(xiàng)目,通過(guò)integration-redis子項(xiàng)目消约,提供了非常豐富的功能肠鲫,關(guān)于該項(xiàng)目,后面有時(shí)間再寫(xiě)篇文章具體分析一下或粮,這里我們用到其中的一個(gè)組件RedisLockRegistry

導(dǎo)入依賴(lài)

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-integration</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.integration</groupId>
    <artifactId>spring-integration-redis</artifactId>
</dependency>

配置RedisLockRegistry

@Configuration
public class RedisLockConfiguration {

    @Bean
    public RedisLockRegistry redisLockRegistry(
        RedisConnectionFactory redisConnectionFactory) {
        // 注意這里的時(shí)間單位是毫秒
        return new RedisLockRegistry(redisConnectionFactory, "registryKey", TIME);
    }
}

RedisLockRegistry相當(dāng)于一個(gè)鎖的管理倉(cāng)庫(kù)导饲,所有的鎖都可以從該倉(cāng)庫(kù)獲取,所有鎖的鍵名為:registryKey:LOCK_NAME,默認(rèn)時(shí)間為60s

配置完鎖的倉(cāng)庫(kù)之后帜消,只需要注入倉(cāng)庫(kù)棠枉,當(dāng)需要使用到鎖的時(shí)候,從倉(cāng)庫(kù)中獲取一個(gè)鎖就可以了泡挺,如下所示

Lock lock = redisLockRegistry.obtain("redis-lock");

該操作返回一個(gè)Lock對(duì)象辈讶,該對(duì)象其實(shí)是Spring實(shí)現(xiàn)的基于Redis的鎖,該鎖支持了豐富的功能娄猫,如tryLock

但使用的時(shí)候贱除,只需要跟普通的鎖一樣操作即可

// lock.tryLock(10, TimeUnit.SECONDS);
lock.lock();
try {
 // ops   
}catch(Exception e) {
    
}finally {
    // 釋放鎖
    lock.unlock();
}

可以看到,通過(guò)RedisLockRegistry媳溺,我們可以更加方便地使用Redis分布式鎖了

RedisLockRegistry源碼分析

上面學(xué)習(xí)了RedisLockRegistry的使用之后月幌,接下來(lái)我們來(lái)具體看下RedisLockRegistry的具體實(shí)現(xiàn)

RedisLockRegistry結(jié)構(gòu)

從上面的繼承結(jié)構(gòu)可以清晰地看出RedisLockRegistry的繼承情況,而上面的幾個(gè)接口基本上都只提供了基本的定義悬蔽,這里就不展開(kāi)分析了扯躺。直接看RedisLockRegistry的實(shí)現(xiàn)

構(gòu)造函數(shù)

首先是構(gòu)造函數(shù),有兩個(gè)構(gòu)造函數(shù)蝎困,如下

private static final long DEFAULT_EXPIRE_AFTER = 60000L;

// 提供了默認(rèn)的的過(guò)期時(shí)間录语,默認(rèn)過(guò)期時(shí)間為60s
public RedisLockRegistry(RedisConnectionFactory connectionFactory, String registryKey) {
    this(connectionFactory, registryKey, DEFAULT_EXPIRE_AFTER);
}

public RedisLockRegistry(RedisConnectionFactory connectionFactory, 
                         String registryKey, 
                         long expireAfter) {
    Assert.notNull(connectionFactory, "'connectionFactory' cannot be null");
    Assert.notNull(registryKey, "'registryKey' cannot be null");
    this.redisTemplate = new StringRedisTemplate(connectionFactory);
    this.obtainLockScript = 
        new DefaultRedisScript<>(OBTAIN_LOCK_SCRIPT, Boolean.class);
    this.registryKey = registryKey;
    this.expireAfter = expireAfter;
    this.unlinkAvailable = RedisUtils.isUnlinkAvailable(this.redisTemplate);
}

上面第二個(gè)構(gòu)造函數(shù)中,有兩個(gè)沒(méi)見(jiàn)過(guò)的屬性禾乘,分別是obtainLockScript以及unlinkAvailable澎埠,分析如下

obtainLockScript

private final RedisScript<Boolean> obtainLockScript;

obtainLockScript = new DefaultRedisScript<>(OBTAIN_LOCK_SCRIPT, Boolean.class);

可以看到obtainLockScript是一個(gè)DefaultRedisScript實(shí)例,該實(shí)例的對(duì)象用于執(zhí)行Lua腳本始藕,具體的看下DefaultRedisScript的源碼

上面的OBTAIN_LOCK_SCRIPT內(nèi)容如下

private static final String OBTAIN_LOCK_SCRIPT =
            "local lockClientId = redis.call('GET', KEYS[1])\n" +
                    "if lockClientId == ARGV[1] then\n" +
                    "  redis.call('PEXPIRE', KEYS[1], ARGV[2])\n" +
                    "  return true\n" +
                    "elseif not lockClientId then\n" +
                    "  redis.call('SET', KEYS[1], ARGV[1], 'PX', ARGV[2])\n" +
                    "  return true\n" +
                    "end\n" +
                    "return false";

可以看到蒲稳,其實(shí)就是一段簡(jiǎn)單的Lua腳本,腳本邏輯如下

  1. 調(diào)用get命令獲取對(duì)應(yīng)的key伍派,如果存在在走2江耀,不存在,則走3
  2. 判斷key的值是否是入?yún)⑺咧玻绻蔷黾牵瑒t調(diào)用pexire設(shè)置過(guò)期時(shí)間,返回true表示加鎖成功
  3. 如果不存在倍踪,則調(diào)用set命令進(jìn)行加鎖系宫,并且設(shè)置過(guò)期時(shí)間,返回true表示加鎖成功建车,從命令中可以看到扩借,使用的參是px,所以構(gòu)造函數(shù)傳入的單位是毫秒而不是秒
  4. 如果沒(méi)有執(zhí)行2缤至、3操作潮罪,則返回false康谆,表示加鎖失敗

isUnlinkAvailable

該函數(shù)檢查對(duì)應(yīng)的redis是否支持UNLINK命令,該命令用于異步刪除某個(gè)鍵嫉到,功能等同于del命令沃暗,但非阻塞,只有在redis4及以上版本才支持

函數(shù)內(nèi)容如下:

public static boolean isUnlinkAvailable(RedisOperations<?, ?> redisOperations) {
        return unlinkAvailable.computeIfAbsent(
            redisOperations, key -> {
                Properties info = redisOperations.execute(
                    (RedisCallback<Properties>) connection -> 
                    connection.serverCommands().info(SECTION));
            if (info != null) {
                String version = info.getProperty(VERSION_PROPERTY);
                if (StringUtils.hasText(version)) {
                    int majorVersion = Integer.parseInt(version.split("\\.")[0]);
                    return majorVersion >= 4;
                }
                else {
                    return false;
                }
            }
            else {
                throw new IllegalStateException("The INFO command cannot be used in pipeline/transaction.");
            }
        });
    }

核心

RedisLockRegistry的核心方法其實(shí)只有一個(gè)何恶,就是obtainLock孽锥,具體實(shí)現(xiàn)如下

private final Map<String, RedisLock> locks = new ConcurrentHashMap<>();

@Override
public Lock obtain(Object lockKey) {
    Assert.isInstanceOf(String.class, lockKey);
    String path = (String) lockKey;
    return this.locks.computeIfAbsent(path, RedisLock::new);
}

可以看到,每一個(gè)LockRegistry自己維護(hù)了一個(gè)LOCK-KEY-LOCK的map,這也表明,同一個(gè)Registry中凳忙,相同的鍵只會(huì)對(duì)應(yīng)一個(gè)Lock對(duì)象

RedisLock

從上面的分析中可以看到,LockRegistry維護(hù)了一個(gè)RedisLock對(duì)象的Map盛撑,鍵是鎖的名稱(chēng),值是對(duì)應(yīng)的Lock對(duì)象捧搞,該對(duì)象是Spring實(shí)現(xiàn)的一個(gè)內(nèi)部類(lèi)抵卫,具體實(shí)現(xiàn)如下所示

構(gòu)造方法

private RedisLock(String path) {
    this.lockKey = constructLockKey(path);
}

RedisLock有且只有一個(gè)私有構(gòu)造方法,所以?xún)H能在當(dāng)前類(lèi)中進(jìn)行構(gòu)造胎撇,這也意味著我們無(wú)法自己實(shí)例化RedisLock實(shí)例

構(gòu)造的過(guò)程非常簡(jiǎn)單介粘,只是初始化了lockKey,lockKey的內(nèi)容如下

private String constructLockKey(String path) {
    return RedisLockRegistry.this.registryKey + ":" + path;
}

可以看到创坞,lockKey的值其實(shí)就是Registry的名稱(chēng) + : + 鎖的名稱(chēng)

核心方法

對(duì)于一把鎖而言,最最核心的方法莫過(guò)于加鎖和解鎖了受葛,RedisLock實(shí)現(xiàn)了Lock接口题涨,提供了多樣的加鎖方式,分別如下所示

不可中斷鎖
private final ReentrantLock localLock = new ReentrantLock();

@Override
public void lock() {
    this.localLock.lock();
    while (true) {
        try {
            while (!obtainLock()) {
                Thread.sleep(100); //NOSONAR
            }
            break;
        }
        catch (InterruptedException e) {
            // 不可中斷总滩,所以忽略中斷異常
        }
        catch (Exception e) {
            this.localLock.unlock();
            rethrowAsLockException(e);
        }
    }
}

從上面的代碼可以看到纲堵,lock方法首先嘗試獲取ReentrantLock,如果獲取成功闰渔,才嘗試去獲取分布式鎖席函,獲取localLock的目的在于,如果本地有多個(gè)線(xiàn)程在競(jìng)爭(zhēng)該鎖冈涧,則只有獲取到本地的鎖的線(xiàn)程才能?chē)L試去獲取分布式鎖茂附,好處在于,減少了不必要的網(wǎng)絡(luò)開(kāi)銷(xiāo)督弓,提高性能

由于lock方法明確規(guī)定营曼,如果獲取不到鎖,則進(jìn)行阻塞愚隧,直至獲取到鎖或者出現(xiàn)異常蒂阱,所以上面每隔100毫秒會(huì)去嘗試獲取鎖,直到獲取成功或者拋出異常為止

獲取鎖的代碼也非常簡(jiǎn)單,如下所示

// 實(shí)例化Registry的時(shí)候進(jìn)行初始化
private final String clientId = UUID.randomUUID().toString();

private boolean obtainLock() {
    Boolean success =
      RedisLockRegistry.this.redisTemplate.execute(
        // 獲取鎖的lua腳本
        RedisLockRegistry.this.obtainLockScript,
        // 獲取的鎖名稱(chēng)
        Collections.singletonList(this.lockKey), 
        // 鎖的內(nèi)容
        RedisLockRegistry.this.clientId,
        // 鎖的過(guò)期時(shí)間
        String.valueOf(RedisLockRegistry.this.expireAfter));

    boolean result = Boolean.TRUE.equals(success);
    
    // 如果獲取成功录煤,則記錄鎖的時(shí)間
    if (result) {
        this.lockedAt = System.currentTimeMillis();
    }
    return result;
}

從上面獲取鎖的代碼可以看到鳄厌,每一個(gè)LockRegistry實(shí)例只會(huì)有一個(gè)值,該值在Registry實(shí)例化的時(shí)候通過(guò)UUID生成妈踊,一個(gè)實(shí)例內(nèi)的多個(gè)線(xiàn)程之間的競(jìng)爭(zhēng)直接通過(guò)ReentrantLock進(jìn)行了嚎,不涉及到Redis相關(guān)的操作。

可中斷鎖
@Override
public void lockInterruptibly() throws InterruptedException {
    this.localLock.lockInterruptibly();
    try {
        while (!obtainLock()) {
            Thread.sleep(100); //NOSONAR
        }
    }
    catch (InterruptedException ie) {
        // 釋放鎖响委,并且響應(yīng)中斷信號(hào)
        this.localLock.unlock();
        Thread.currentThread().interrupt();
        throw ie;
    }
    catch (Exception e) {
        this.localLock.unlock();
        rethrowAsLockException(e);
    }
}

看懂了lock的代碼新思,再來(lái)看lockInterruptibly就非常簡(jiǎn)單了,lock不響應(yīng)中斷信號(hào)赘风,則lockInterruptibly則相應(yīng)中斷信號(hào)夹囚,因此,獲取鎖的過(guò)程如果出現(xiàn)中斷邀窃,則結(jié)束獲取操作了

嘗試獲取鎖

嘗試獲取鎖以為著如果能獲取鎖荸哟,則獲取,如果不能獲取瞬捕,則結(jié)束鞍历,當(dāng)然,可以附帶等待是時(shí)間肪虎,有兩個(gè)版本的tryLock劣砍,如下

@Override
public boolean tryLock() {
    try {
        // 調(diào)用另一個(gè)tryLock,并且將時(shí)間設(shè)置為0
        return tryLock(0, TimeUnit.MILLISECONDS);
    }
    catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        return false;
    }
}

@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
    long now = System.currentTimeMillis();
    // 先嘗試獲取本地鎖扇救,如果在指定時(shí)間內(nèi)無(wú)法獲取到本地鎖刑枝,則放棄
    if (!this.localLock.tryLock(time, unit)) {
        return false;
    }
    try {
        
        // 記錄獲取鎖到期時(shí)間
        long expire = now + TimeUnit.MILLISECONDS.convert(time, unit);
        boolean acquired;
        
        // 如果獲取不到鎖,并且時(shí)間還有剩余迅腔,則先休眠100毫秒装畅,然后繼續(xù)嘗試獲取
        while (!(acquired = obtainLock()) && System.currentTimeMillis() < expire) { 
            Thread.sleep(100); //NOSONAR
        }
        // 到這里表示獲取鎖超時(shí)
        // 如果無(wú)法獲取到分布式鎖,則釋放本地鎖
        if (!acquired) {
            this.localLock.unlock();
        }
        return acquired;
    }
    catch (Exception e) {
        this.localLock.unlock();
        rethrowAsLockException(e);
    }
    return false;
}

具體的分析都詳細(xì)寫(xiě)在注釋里了沧烈,補(bǔ)充一點(diǎn)就是掠兄,從tryLock的實(shí)現(xiàn)中可以看到,tryLock本身是響應(yīng)中斷的锌雀,與接口的定義一致

釋放鎖
// 判斷鎖的所有者是否是當(dāng)前實(shí)例
public boolean isAcquiredInThisProcess() {
    return RedisLockRegistry.this.clientId.equals(
        RedisLockRegistry.this.redisTemplate.boundValueOps(this.lockKey).get());
}

// 刪除對(duì)應(yīng)的鍵蚂夕,也即釋放分布式鎖
private void removeLockKey() {
    if (this.unlinkAvailable) {
        RedisLockRegistry.this.redisTemplate.unlink(this.lockKey);
    }
    else {
        RedisLockRegistry.this.redisTemplate.delete(this.lockKey);
    }
}

@Override
public void unlock() {
    // 如果嘗試釋放的不是本線(xiàn)程加的鎖,則拋出異常
    if (!this.localLock.isHeldByCurrentThread()) {
        throw new IllegalStateException("You do not own lock at " + this.lockKey);
    }
    // 當(dāng)前線(xiàn)程持有的鎖的數(shù)量腋逆,即重入的次數(shù)
    // 如果此時(shí) > 1双抽,表示當(dāng)前線(xiàn)程有多次獲取鎖,釋放的時(shí)候只減少本地鎖的次數(shù)
    // 此時(shí)其他的方法還持有鎖闲礼,不能釋放分布式鎖
    if (this.localLock.getHoldCount() > 1) {
        this.localLock.unlock();
        return;
    }
    try {
        // 此時(shí)分布式鎖已經(jīng)由于超時(shí)被釋放了牍汹,拋出異常
        if (!isAcquiredInThisProcess()) {
            throw new IllegalStateException("Lock was released in the store due to expiration. " +
 "The integrity of data protected by this lock may have been compromised.");
        }
        
        // 如果收到中斷信號(hào)铐维,則異步釋放鎖
        // 盡快響應(yīng)中斷...
        if (Thread.currentThread().isInterrupted()) {
            RedisLockRegistry.this.executor.execute(this::removeLockKey);
        }
        else {
            removeLockKey();
        }

        if (logger.isDebugEnabled()) {
            logger.debug("Released lock; " + this);
        }
    }
    catch (Exception e) {
        ReflectionUtils.rethrowRuntimeException(e);
    }
    finally {
        this.localLock.unlock();
    }
}

總結(jié)

本文主要簡(jiǎn)單介紹了分布式鎖,在Redis中使用分布式鎖的原理慎菲,本質(zhì)就是set或者setnx命令的使用嫁蛇,以及對(duì)應(yīng)版本的加鎖以及解鎖操作。

最后分析了RedisLockRegistry的具體實(shí)現(xiàn)露该,RedisLockRegistry是Spring提供的基于Redis的分布式鎖的實(shí)現(xiàn)睬棚,主要包含兩部分,一部分是本地鎖解幼,用于一個(gè)實(shí)例下多個(gè)線(xiàn)程的協(xié)調(diào)抑党,只有獲取到本地鎖的線(xiàn)程才去嘗試獲取分布式鎖,通過(guò)這種方式來(lái)提高獲取鎖的性能

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末撵摆,一起剝皮案震驚了整個(gè)濱河市底靠,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌特铝,老刑警劉巖暑中,帶你破解...
    沈念sama閱讀 219,270評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異鲫剿,居然都是意外死亡鳄逾,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)灵莲,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)雕凹,“玉大人,你說(shuō)我怎么就攤上這事政冻∶兜郑” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,630評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵赠幕,是天一觀(guān)的道長(zhǎng)俄精。 經(jīng)常有香客問(wèn)我询筏,道長(zhǎng)榕堰,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,906評(píng)論 1 295
  • 正文 為了忘掉前任嫌套,我火速辦了婚禮逆屡,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘踱讨。我一直安慰自己魏蔗,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,928評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布痹筛。 她就那樣靜靜地躺著莺治,像睡著了一般廓鞠。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上谣旁,一...
    開(kāi)封第一講書(shū)人閱讀 51,718評(píng)論 1 305
  • 那天床佳,我揣著相機(jī)與錄音,去河邊找鬼榄审。 笑死砌们,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的搁进。 我是一名探鬼主播浪感,決...
    沈念sama閱讀 40,442評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼饼问!你這毒婦竟也來(lái)了影兽?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,345評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤匆瓜,失蹤者是張志新(化名)和其女友劉穎赢笨,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體驮吱,經(jīng)...
    沈念sama閱讀 45,802評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡茧妒,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,984評(píng)論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了左冬。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片桐筏。...
    茶點(diǎn)故事閱讀 40,117評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖拇砰,靈堂內(nèi)的尸體忽然破棺而出梅忌,到底是詐尸還是另有隱情,我是刑警寧澤除破,帶...
    沈念sama閱讀 35,810評(píng)論 5 346
  • 正文 年R本政府宣布牧氮,位于F島的核電站,受9級(jí)特大地震影響瑰枫,放射性物質(zhì)發(fā)生泄漏踱葛。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,462評(píng)論 3 331
  • 文/蒙蒙 一光坝、第九天 我趴在偏房一處隱蔽的房頂上張望尸诽。 院中可真熱鬧,春花似錦盯另、人聲如沸性含。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,011評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)商蕴。三九已至叠萍,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間绪商,已是汗流浹背俭令。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,139評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留部宿,地道東北人抄腔。 一個(gè)月前我還...
    沈念sama閱讀 48,377評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像理张,于是被迫代替她去往敵國(guó)和親赫蛇。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,060評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容