雪花算法(snowflake)生成Id重復(fù)問(wèn)題

前言

最近工作上遇到一個(gè)雪花算法生成Id重復(fù)導(dǎo)致數(shù)據(jù)庫(kù)中表主鍵沖突,導(dǎo)致入庫(kù)失敗的問(wèn)題苍匆,所以順便學(xué)習(xí)了一下雪花算法顾孽,下面是學(xué)習(xí)的筆記以及討論如果解決雪花算法在分布式部署中生成重復(fù)Id的問(wèn)題。

基礎(chǔ)概念

snowflake中文的意思是雪花窖杀,所以常被稱(chēng)為雪花算法

它是twitter用scala語(yǔ)言編寫(xiě)的一個(gè)用于簡(jiǎn)單規(guī)則運(yùn)算就能高效生成唯一ID的算法蝴猪,下面是源碼地址:

github源碼地址

網(wǎng)上還有各種其他語(yǔ)言的版本调衰,思路基本上都是參考上述源碼

特性

生成的ID不重復(fù)
生成性能高
基于時(shí)間戳膊爪,可以基本保證有序遞增

設(shè)計(jì)原理

準(zhǔn)備工作

bit與byte
bit(位):電腦中存儲(chǔ)的最小單位,可以存儲(chǔ)二進(jìn)制中的0或1
byte(字節(jié)):一個(gè)byte由8個(gè)bit組成
如圖:

byte和bit.png

而在java中嚎莉,每個(gè)數(shù)據(jù)類(lèi)型存儲(chǔ)所占的字節(jié)數(shù)不一樣米酬,常用的如下:
int:4 個(gè)字節(jié)。
short:2 個(gè)字節(jié)趋箩。
long:8 個(gè)字節(jié)赃额。
byte:1 個(gè)字節(jié)。
float:4 個(gè)字節(jié)叫确。
double:8 個(gè)字節(jié)跳芳。
char:2 個(gè)字節(jié)。

而雪花算法生成的數(shù)字竹勉,我們定義為long飞盆,所以就是8個(gè)byte,64bit
假設(shè)我們定義 long a = 1L次乓;則在計(jì)算機(jī)中的存儲(chǔ)如下:


long類(lèi)型的存儲(chǔ).png

也就是可表示的范圍為:-9223372036854775808(-2的63次方) ~ 9223372036854775807(2的63次方-1)吓歇,考慮到生成的唯一值用于數(shù)據(jù)庫(kù)主鍵,所以理論值為0~9223372036854775807(2的63次方-1)票腰,容量上肯定能滿(mǎn)足業(yè)務(wù)方了

組成原理

雪花算法生成的Id由:1bit 不用 + 41bit時(shí)間戳+10bit工作機(jī)器id+12bit序列號(hào)城看,如下圖:

雪花ID的組成

不用:1bit,因?yàn)樽罡呶皇欠?hào)位杏慰,0表示正析命,1表示負(fù),所以這里固定為0
時(shí)間戳:41bit逃默,服務(wù)上線的時(shí)間毫秒級(jí)的時(shí)間戳(為當(dāng)前時(shí)間-服務(wù)第一次上線時(shí)間),這里為(2^41-1)/1000/60/60/24/365 = 49.7年
工作機(jī)器id:10bit簇搅,表示工作機(jī)器id完域,用于處理分布式部署id不重復(fù)問(wèn)題,可支持2^10 = 1024個(gè)節(jié)點(diǎn)
序列號(hào):12bit瘩将,用于離散同一機(jī)器同一毫秒級(jí)別生成多條Id時(shí)吟税,可允許同一毫秒生成2^12 = 4096個(gè)Id,則一秒就可生成4096*1000 = 400w個(gè)Id

說(shuō)明:上面總體是64位姿现,具體位數(shù)可自行配置肠仪,如想運(yùn)行更久,需要增加時(shí)間戳位數(shù)备典;如想支持更多節(jié)點(diǎn)异旧,可增加工作機(jī)器id位數(shù);如想支持更高并發(fā)提佣,增加序列號(hào)位數(shù)

Java版本的具體實(shí)現(xiàn)

public class SnowflakeIdWorker {
    /** 開(kāi)始時(shí)間截 (建議用服務(wù)第一次上線的時(shí)間吮蛹,到毫秒級(jí)的時(shí)間戳) */
    private final long twepoch = 687888001020L;

    /** 機(jī)器id所占的位數(shù) */
    private final long workerIdBits = 10L;

    /** 支持的最大機(jī)器id荤崇,結(jié)果是1023 (這個(gè)移位算法可以很快的計(jì)算出幾位二進(jìn)制數(shù)所能表示的最大十進(jìn)制數(shù)) */
    private final long maxWorkerId = -1L ^ (-1L << workerIdBits);

    /** 序列在id中占的位數(shù) */
    private final long sequenceBits = 12L;

    /** 機(jī)器ID向左移12位 */
    private final long workerIdShift = sequenceBits;

    /** 時(shí)間截向左移22位(10+12) */
    private final long timestampLeftShift = sequenceBits + workerIdBits;

    /** 生成序列的掩碼,這里為4095 (0b111111111111=0xfff=4095)
     * <<為左移潮针,每左移動(dòng)1位术荤,則擴(kuò)大1倍
     * */
    private final long sequenceMask = -1L ^ (-1L << sequenceBits);

    /** 工作機(jī)器ID(0~1024) */
    private long workerId;

    /** 毫秒內(nèi)序列(0~4095) */
    private long sequence = 0L;

    /** 上次生成ID的時(shí)間截 */
    private long lastTimestamp = -1L;

    //==============================Constructors=====================================
    /**
     * 構(gòu)造函數(shù)
     * @param workerId 工作ID (0~1023)
     */
    public SnowflakeIdWorker(long workerId) {
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException(String.format("workerId can't be greater than %d or less than 0", maxWorkerId));
        }
        this.workerId = workerId;
    }

    // ==============================Methods==========================================
    /**
     * 獲得下一個(gè)ID (該方法是線程安全的)
     * @return SnowflakeId
     */
    public synchronized long nextId() {
        long timestamp = timeGen();
        //如果當(dāng)前時(shí)間小于上一次ID生成的時(shí)間戳,說(shuō)明系統(tǒng)時(shí)鐘回退過(guò)這個(gè)時(shí)候應(yīng)當(dāng)拋出異常
        if (timestamp < lastTimestamp) {
            throw new RuntimeException(
                    String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
        }

        //如果是同一時(shí)間生成的每篷,則進(jìn)行毫秒內(nèi)序列
        if (lastTimestamp == timestamp) {
            //如果毫秒相同瓣戚,則從0遞增生成序列號(hào)
            sequence = (sequence + 1) & sequenceMask;
            //毫秒內(nèi)序列溢出
            if (sequence == 0) {
                //阻塞到下一個(gè)毫秒,獲得新的時(shí)間戳
                timestamp = tilNextMillis(lastTimestamp);
            }
        }
        //時(shí)間戳改變,毫秒內(nèi)序列重置
        else {
            sequence = 0L;
        }

        //上次生成ID的時(shí)間截
        lastTimestamp = timestamp;

        //移位并通過(guò)或運(yùn)算拼到一起組成64位的ID
        return ((timestamp - twepoch) << timestampLeftShift) //
                | (workerId << workerIdShift) //
                | sequence;
    }

    /**
     * 阻塞到下一個(gè)毫秒焦读,直到獲得新的時(shí)間戳
     * @param lastTimestamp 上次生成ID的時(shí)間截
     * @return 當(dāng)前時(shí)間戳
     */
    protected long tilNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }

    /**
     * 返回以毫秒為單位的當(dāng)前時(shí)間子库,從1970-01-01 08:00:00算起
     * @return 當(dāng)前時(shí)間(毫秒)
     */
    protected long timeGen() {
        return System.currentTimeMillis();
    }
}

上述代碼中,有涉及到位運(yùn)算吨灭,這里對(duì)雪花算法中需要用到的挑出來(lái)介紹一下:

原碼刚照、反碼、補(bǔ)碼

我們?yōu)槭裁匆肋@三個(gè)概念呢喧兄?首先要知道无畔,計(jì)算機(jī)中的運(yùn)算都是以補(bǔ)碼的形式進(jìn)行運(yùn)算的

原碼就是二進(jìn)制的形式,反碼和補(bǔ)碼跟本身的正負(fù)有關(guān)吠冤,定義如下:

類(lèi)型 原碼 反碼 補(bǔ)碼
正數(shù) 二進(jìn)制 就是原碼 就是原碼
負(fù)數(shù) 二進(jìn)制 符號(hào)位不變浑彰,其他位取反 反碼的基礎(chǔ)上加1

我們先來(lái)看原碼,數(shù)字轉(zhuǎn)換成二進(jìn)制就是這個(gè)數(shù)字的原碼拯辙,比如之前提到的long a = 1L郭变;如下:


1的源碼.png

要注意的是最高位是符號(hào)位,1標(biāo)識(shí)負(fù)涯保,0表示正诉濒,則long a = -1L的原碼,如下:


-1的原碼.png

long a = 1L夕春,反碼和補(bǔ)碼是跟原碼一致未荒,我們主要來(lái)看-1L的情況:


-1的原碼、反碼及志、補(bǔ)碼.png

左移<<

a << b片排, 表示a的二進(jìn)制數(shù)值整體向左移動(dòng)b位,符號(hào)位不變速侈,低位空出來(lái)的補(bǔ)0率寡,相當(dāng)于a * (2^b)

比如-1L << 12, 表示-1L的二進(jìn)制往左移動(dòng)12位倚搬,剛才提了負(fù)數(shù)的二進(jìn)制是以補(bǔ)碼的形式存在冶共,則運(yùn)算過(guò)程如下:

-1L左移12.png

異或^

規(guī)則 兩個(gè)操作數(shù)進(jìn)行異或時(shí),對(duì)于同一位上,如果數(shù)值相同則為 0比默,數(shù)值不同則為 1幻捏。
1 ^ 0 = 1,
1 ^ 1 = 0,
0 ^ 0 = 0;
比如,-1L ^(-1L << 12),也就是-1L ^-4096命咐,運(yùn)算過(guò)程如下:


-1異或-4096.png

或 |

規(guī)則 或運(yùn)算時(shí)篡九,進(jìn)行運(yùn)算的兩個(gè)數(shù),從最低位到最高位醋奠,一一對(duì)應(yīng)榛臼。如果某 bit 的兩個(gè)數(shù)值對(duì)應(yīng)的值只要 1 個(gè)為 1,則結(jié)果值相應(yīng)的 bit 就是 1窜司,否則為 0沛善。
0 | 0 = 0,
0 | 1 = 1,
1 | 1 = 1

比如:3 | 5
如下圖:


3或5.png

如果想了解得更詳細(xì),可以看:位運(yùn)算

雪花算法的細(xì)節(jié)

1. 線程安全

    /**
     * 獲得下一個(gè)ID (該方法是線程安全的)
     * @return SnowflakeId
     */
    public synchronized long nextId() {

可以看到塞祈,生成ID的方法是加了synchronized 關(guān)鍵詞金刁,確保了線程安全,否則在并發(fā)情況下议薪,生成的Id就有可能重復(fù)了

2. 同一毫秒,生成多個(gè)Id時(shí)

根據(jù)雪花算法的組成尤蛮,可以看出,如果同一臺(tái)機(jī)器同一毫秒需要生成多個(gè)Id斯议,因?yàn)楹撩氲臅r(shí)間戳产捞、機(jī)器工作id一樣,則前52位一致哼御,所以需要靠后12位的序列號(hào)來(lái)區(qū)分


image.png

具體關(guān)鍵性代碼如下:

        //如果是同一時(shí)間生成的坯临,則進(jìn)行毫秒內(nèi)序列
        if (lastTimestamp == timestamp) {
            //如果毫秒相同,則從0遞增生成序列號(hào)
            sequence = (sequence + 1) & sequenceMask;
            //毫秒內(nèi)序列溢出
            if (sequence == 0) {
                //阻塞到下一個(gè)毫秒,獲得新的時(shí)間戳
                timestamp = tilNextMillis(lastTimestamp);
            }
        }

lastTimestamp 記錄了上一次生成Id的毫秒級(jí)的時(shí)間戳恋昼;timestamp為當(dāng)前生成Id時(shí)毫秒級(jí)的時(shí)間戳看靠,如果同一毫秒生成多個(gè)id,則兩者相等

然后通過(guò)下面的代碼來(lái)生成序列

//如果毫秒相同液肌,則從0遞增生成序列號(hào)
sequence = (sequence + 1) & sequenceMask;

sequence開(kāi)始為0衷笋,sequenceMask為生成序列號(hào)的掩碼,定義如下:

    /** 生成序列的掩碼矩屁,這里為4095 (0b111111111111=0xfff=4095)
     * <<為左移,每左移動(dòng)1位爵赵,則擴(kuò)大1倍
     * */
    private final long sequenceMask = -1L ^ (-1L << sequenceBits);

上述的sequenceBits為序列號(hào)吝秕,這里定義的為12,則要運(yùn)算下面代碼

-1L ^(-1L << 12)

我們?cè)谏厦娈惢虻倪\(yùn)算中空幻,有算過(guò)這個(gè)值烁峭,為4095,不記得的可以看上面 異或部分,也就是同一毫秒约郁,可以逐步生成0~4095序列號(hào)

如果sequence遞增到4095重新回到0時(shí)缩挑,證明當(dāng)前毫秒已經(jīng)產(chǎn)生了4096個(gè)序列號(hào),則使用tilNextMillis(lastTimestamp)方法阻塞到下一毫秒并賦值給timestamp鬓梅,此時(shí)sequence=0供置,我們看看tilNextMillis(lastTimestamp)是怎么阻塞到下一毫秒的

    /**
     * 阻塞到下一個(gè)毫秒,直到獲得新的時(shí)間戳
     * @param lastTimestamp 上次生成ID的時(shí)間截
     * @return 當(dāng)前時(shí)間戳
     */
    protected long tilNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }

可以看到绽快,直接就是不斷獲取當(dāng)前時(shí)間和最近生成Id的時(shí)間戳進(jìn)行判斷芥丧,如果還在當(dāng)前毫秒級(jí)別,則空轉(zhuǎn)坊罢,直到下一毫秒

3.移位并通過(guò)或運(yùn)算拼到一起組成64位的ID

        //移位并通過(guò)或運(yùn)算拼到一起組成64位的ID
        return ((timestamp - twepoch) << timestampLeftShift) //
                | (workerId << workerIdShift) //
                | sequence;

我們?cè)倩仡櫹?4位的Id是怎么組成的

ID的組成

那我們?cè)趺赐ㄟ^(guò)時(shí)間戳续担、工作機(jī)器id、序列號(hào)來(lái)完成拼接呢活孩?其實(shí)就是通過(guò)移位并或運(yùn)算來(lái)完成的物遇,我們先看下上述代碼中的含義:
timestamp :當(dāng)前時(shí)間毫秒級(jí)別的時(shí)間戳
twepoch:開(kāi)始時(shí)間毫秒級(jí)別的時(shí)間截
timestampLeftShift:時(shí)間需要左移位數(shù),這里為sequenceBits + workerIdBits憾儒,這里為序列號(hào)位數(shù)+工作機(jī)器id位數(shù)询兴,即12+10 = 22
workerId :工作機(jī)器id,用于解決分布式Id重復(fù)的問(wèn)題航夺,這里為外部傳入的參數(shù)
workerIdShift:工作機(jī)器id左移位數(shù)蕉朵,這里為sequenceBits,即12
sequence:序列阳掐,這里為0~4095中的一個(gè)數(shù)值

我們舉個(gè)例子始衅,假設(shè)twepoch為當(dāng)前時(shí)間,timestamp為twepoch之后1000ms缭保,即(timestamp - twepoch)=1000汛闸;工作機(jī)器id為1,即workerId = 1艺骂;當(dāng)前毫秒值第一次生成诸老,即sequence = 0,則ID為:
((1000) << 22)
| (1 << 12)
| 0
即生成的Id:4194308096
我們先看1000钳恕、1别伏、0的二進(jìn)制,以及進(jìn)行位移并或運(yùn)算之后的結(jié)果

運(yùn)算結(jié)果0.png

假設(shè)同一毫秒值忧额,又生成了一次id厘肮,則:
((1000) << 22)
| (1 << 12)
| 1
即生成的Id:4194308097,所以同一臺(tái)機(jī)器人上基本保證了遞增

雪花算法生成Id重復(fù)問(wèn)題

我們之前提到睦番,同一機(jī)器同一毫秒級(jí)类茂,我們能生成4096個(gè)不同序列耍属,即不同Id,但是如果我們使用的是微服務(wù)架構(gòu)巩检,那不同機(jī)器人是否會(huì)可能生成相同Id呢厚骗?

ID的組成

其實(shí)我們之前有提到工作機(jī)器Id的作用,就是用于解決分布式Id重復(fù)的問(wèn)題兢哭,這個(gè)workerId是通過(guò)構(gòu)造方法傳入的领舰,如果我們用10位來(lái)存儲(chǔ)這個(gè)值,那就是最多支持1024個(gè)節(jié)點(diǎn)

    /**
     * 構(gòu)造函數(shù)
     * @param workerId 工作ID (0~1023)
     */
    public SnowflakeIdWorker(long workerId) {
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException(String.format("workerId can't be greater than %d or less than 0", maxWorkerId));
        }
        this.workerId = workerId;
    }

那么關(guān)鍵問(wèn)題就回歸到如何去把我們的服務(wù)器和workerId對(duì)應(yīng)起來(lái)厦瓢?如果不是容器化部署提揍,部署是固定的機(jī)器,我們用機(jī)器的唯一名來(lái)做key煮仇,那我們可以對(duì)這些機(jī)器名和workerId建立一個(gè)對(duì)應(yīng)關(guān)系劳跃,如果存在就用之前的workerId,不存在就往上累加比如我們用計(jì)算機(jī)名做key:

image.png

非容器化部署.png

這樣機(jī)器如果不斷累加浙垫,最多支持1024臺(tái)服務(wù)器

但是如果是容器化部署刨仑,需要支持動(dòng)態(tài)增加節(jié)點(diǎn),并且每次部署的機(jī)器不一定一樣時(shí)夹姥,就會(huì)有問(wèn)題杉武,如果發(fā)現(xiàn)不同,就往上累加辙售,經(jīng)過(guò)多次發(fā)版轻抱,就可能會(huì)超過(guò)1023,這個(gè)時(shí)候生成雪花Id時(shí)旦部,工作機(jī)器id左移12位后肺然,當(dāng)進(jìn)行或運(yùn)算時(shí)罐旗,時(shí)間戳的位置就會(huì)被影響捞奕,比如workerId=1024琅拌,我們拿之前的舉例第1000ms,那它和第1001ms婚度、workerId=0配置蘸秘,可能生成重復(fù)的Id,如下圖所示:


機(jī)器id溢出情況.png

先來(lái)看看我司之前生成workerId的規(guī)則:

        private static void Init()
        {
            if (worker == null)
            {
                //初始化為1
                long workerId = 1;
                //得到服務(wù)器機(jī)器名稱(chēng)
                string hostName = System.Net.Dns.GetHostName();
                if (RedisHelper.Exists(hostName))
                {
                    // 如果redis中存在改服務(wù)器名稱(chēng)蝗茁,則直接取得workerId
                    workerId =long.Parse(RedisHelper.Get(hostName));
                }
                else
                {
                    //如果redis不存在醋虏,則用hashcode對(duì)32取模
                    var code = hostName.GetHashCode();
                    var Id = code % 32;
                    //如果取模以后的Id,大于15哮翘,則從0~15中隨機(jī)一個(gè)數(shù)字灰粮,也就是把16~31中轉(zhuǎn)換到0~15,并存入redis
                    //原因是忍坷,我司只給了4個(gè)bit存儲(chǔ)workerId,所以只能支持0~15
                    if (Id>15||Id<0)
                    {
                        Id = new Random().Next(0, 15);
                    }
                    workerId = (long)Id;
                    RedisHelper.Set(hostName, workerId);
                }
                //把workerId傳入構(gòu)造方法
                worker = new IdWorker(workerId);
            }
        }

上述代碼有2個(gè)問(wèn)題:

  1. hashcode對(duì)32取模,本身就可能會(huì)重復(fù)佩研,比如460141958和3164804對(duì)32取模都是4柑肴,那生成的workerId就重復(fù)了
  2. 如果hashcode>15,隨機(jī)取一個(gè),那每次都有1/16的概率重復(fù)

我司考慮的優(yōu)化方案為:

  1. 在redis中存儲(chǔ)一個(gè)當(dāng)前workerId的最大值
  2. 每次生成workerId時(shí)旬薯,從redis中獲取到當(dāng)前workerId最大值晰骑,并+1作為當(dāng)前workerId,并存入redis
  3. 如果workerId為1023绊序,自增為1024硕舆,則重置0,作為當(dāng)前workerId骤公,并存入redis

上述邏輯抚官,其實(shí)可以參考序列號(hào)的位運(yùn)算,簡(jiǎn)化為:
workerId= (workerId+ 1) & (-1L ^ (-1L << workerIdBits))
其中:workerIdBits為機(jī)器人Id所占的位數(shù)
如果workerIdBits = 10阶捆,則為0增長(zhǎng)到1023后凌节,繼續(xù)從0開(kāi)始自增

上述方案確保了,任何時(shí)間點(diǎn)不同服務(wù)器的workerId一定不一致洒试,假設(shè)我們有6個(gè)pod倍奢,多輪啟動(dòng)的情況如下:


確保同一時(shí)間點(diǎn)workerId不一致.png

上述方案是否一定沒(méi)問(wèn)題呢?其實(shí)是有的垒棋,如果自增1新分配的workerId還沒(méi)釋放掉卒煞,這個(gè)時(shí)候就會(huì)沖突了

比如我們第一個(gè)pod(workerId=0)一直沒(méi)有重啟過(guò),但是第二個(gè)pod一直在重啟叼架,達(dá)到1024時(shí)回到0畔裕,則同時(shí)會(huì)有兩個(gè)pod的workerId為0, 這兩臺(tái)pod上程序生成的Id就有可能重復(fù)。

我們算極端情況下碉碉,workerIdBits=10柴钻,即1024個(gè)節(jié)點(diǎn)的情況下,可以支持到兩次發(fā)版中間第一個(gè)pod一直不重啟垢粮,其余5個(gè)pod一直重啟的極端情況下贴届,也能支持204次。但是只要發(fā)一次版本蜡吧,所有pod都會(huì)到最近redis中記錄的最大workerId毫蚓,像我們一周一個(gè)版本的情況,不會(huì)存在這個(gè)問(wèn)題昔善。


部分pod不重啟的情況.png

我們主要是關(guān)注pod個(gè)數(shù)和workerId運(yùn)行的最大值元潘,如果想支持兩次發(fā)版間更多次非所有pod的重啟,我們可以擴(kuò)充workerIdBits君仆,比如workerIdBits=10翩概,支持workerId最大為1023牲距,但如果workerIdBits=12,則支持workerId最大為4095

幾個(gè)注意的點(diǎn)

  1. twepoch為開(kāi)始的時(shí)間戳钥庇,建議為服務(wù)第一次上線的時(shí)間牍鞠,雖然我們41bit的時(shí)間戳支持49.7年,但是其實(shí)是說(shuō)的距離twepoch的時(shí)間评姨,如果兩者差值超過(guò)了49.7年难述,左右左移22位,就會(huì)導(dǎo)致部分有效數(shù)據(jù)丟失吐句,生成的Id數(shù)據(jù)不能保證大致是遞增的
  2. 雪花算法生成的id的組成位數(shù)胁后,可以根據(jù)自己的實(shí)際需求可調(diào)整,如果需要支持更長(zhǎng)嗦枢,增加時(shí)間戳所占位數(shù)攀芯;如果想支持更多服務(wù)器或者更多次重啟,增加工作機(jī)器人id所占位數(shù)净宵;如果想支持同一時(shí)間更多并發(fā)敲才,增加序列號(hào)所占位數(shù)
  3. 生成雪花算法的類(lèi),需要使用單例模式择葡,并且需要保證線程安全

workerId重復(fù)的其他方案

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末紧武,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子敏储,更是在濱河造成了極大的恐慌阻星,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件已添,死亡現(xiàn)場(chǎng)離奇詭異妥箕,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)更舞,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)畦幢,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人缆蝉,你說(shuō)我怎么就攤上這事宇葱。” “怎么了刊头?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵黍瞧,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我原杂,道長(zhǎng)印颤,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任穿肄,我火速辦了婚禮年局,結(jié)果婚禮上际看,老公的妹妹穿的比我還像新娘。我一直安慰自己矢否,他們只是感情好仿村,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著兴喂,像睡著了一般。 火紅的嫁衣襯著肌膚如雪焚志。 梳的紋絲不亂的頭發(fā)上衣迷,一...
    開(kāi)封第一講書(shū)人閱讀 49,144評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音酱酬,去河邊找鬼壶谒。 笑死,一個(gè)胖子當(dāng)著我的面吹牛膳沽,可吹牛的內(nèi)容都是我干的汗菜。 我是一名探鬼主播,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼挑社,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼陨界!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起痛阻,我...
    開(kāi)封第一講書(shū)人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤菌瘪,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后阱当,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體俏扩,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年弊添,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了录淡。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡油坝,死狀恐怖嫉戚,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情免钻,我是刑警寧澤彼水,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站极舔,受9級(jí)特大地震影響凤覆,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜拆魏,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一盯桦、第九天 我趴在偏房一處隱蔽的房頂上張望慈俯。 院中可真熱鬧,春花似錦拥峦、人聲如沸贴膘。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)刑峡。三九已至,卻和暖如春玄柠,著一層夾襖步出監(jiān)牢的瞬間突梦,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工羽利, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留宫患,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓这弧,卻偏偏與公主長(zhǎng)得像娃闲,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子匾浪,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345

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