[一個(gè)簡單的秒殺架構(gòu)的演變]4. 使用分布式限流

地址

目錄

1. 思路介紹

之前說到樂觀鎖更新操作還是執(zhí)行了近 100 次 SQL抚芦,其實(shí)這 100 次里就只有 10 次扣庫存成功才是有效請求样悟,其他的都是無效請求鸟顺,為了遵從最后落地到數(shù)據(jù)庫的請求數(shù)要盡量少的原則珍坊,這里我們使用限流理肺,把大部分無效請求攔截,盡可能保證最終到達(dá)數(shù)據(jù)庫的都是有效請求

這次我們引入限流闷哆,這里可以先查看一篇文章: 高并發(fā)下的限流分析

看完可以了解幾種限流算法(計(jì)數(shù)器(時(shí)間窗口)礁遵,漏桶,令牌桶)以及區(qū)別鸡典,對比下來源请,我們這里使用固定時(shí)間窗口最好枪芒,這里使用 Redis + Lua 的分布式限流方式

2. 限流實(shí)現(xiàn)

先寫一個(gè)工具類彻况,再寫一個(gè)注解封裝,兩種形式都可以使用

2.1. Lua腳本

  • 秒級限流(每秒限制多少請求)
-- 實(shí)現(xiàn)原理
-- 每次請求都將當(dāng)前時(shí)間舅踪,精確到秒作為 key 放入 Redis 中
-- 超時(shí)時(shí)間設(shè)置為 2s纽甘, Redis 將該 key 的值進(jìn)行自增
-- 當(dāng)達(dá)到閾值時(shí)返回錯(cuò)誤,表示請求被限流
-- 寫入 Redis 的操作用 Lua 腳本來完成
-- 利用 Redis 的單線程機(jī)制可以保證每個(gè) Redis 請求的原子性

-- 資源唯一標(biāo)志位
local key = KEYS[1]
-- 限流大小
local limit = tonumber(ARGV[1])

-- 獲取當(dāng)前流量大小
local currentLimit = tonumber(redis.call('get', key) or "0")

if currentLimit + 1 > limit then
    -- 達(dá)到限流大小 返回
    return 0;
else
    -- 沒有達(dá)到閾值 value + 1
    redis.call("INCRBY", key, 1)
    -- 設(shè)置過期時(shí)間
    redis.call("EXPIRE", key, 2)
    return currentLimit + 1
end
  • 自定義參數(shù)限流(自定義多少時(shí)間限制多少請求)
-- 實(shí)現(xiàn)原理
-- 每次請求都去 Redis 取到當(dāng)前限流開始時(shí)間和限流累計(jì)請求數(shù)
-- 判斷限流開始時(shí)間加超時(shí)時(shí)間戳(限流時(shí)間)大于當(dāng)前請求時(shí)間戳
-- 再判斷當(dāng)前時(shí)間窗口請求內(nèi)是否超過限流最大請求數(shù)
-- 當(dāng)達(dá)到閾值時(shí)返回錯(cuò)誤抽碌,表示請求被限流悍赢,否則通過
-- 寫入 Redis 的操作用 Lua 腳本來完成
-- 利用 Redis 的單線程機(jī)制可以保證每個(gè) Redis 請求的原子性

-- 一個(gè)時(shí)間窗口開始時(shí)間(限流開始時(shí)間)key名稱
local timeKey = KEYS[1]
-- 一個(gè)時(shí)間窗口內(nèi)請求的數(shù)量累計(jì)(限流累計(jì)請求數(shù))key名稱
local requestKey = KEYS[2]
-- 限流大小,限流最大請求數(shù)
local maxRequest = tonumber(ARGV[1])
-- 當(dāng)前請求時(shí)間戳
local nowTime = tonumber(ARGV[2])
-- 超時(shí)時(shí)間戳货徙,一個(gè)時(shí)間窗口時(shí)間(毫秒)(限流時(shí)間)
local timeRequest = tonumber(ARGV[3])

-- 獲取限流開始時(shí)間左权,不存在為0
local currentTime = tonumber(redis.call('get', timeKey) or "0")
-- 獲取限流累計(jì)請求數(shù),不存在為0
local currentRequest = tonumber(redis.call('get', requestKey) or "0")

-- 判斷當(dāng)前請求時(shí)間戳是不是在當(dāng)前時(shí)間窗口中
-- 限流開始時(shí)間加超時(shí)時(shí)間戳(限流時(shí)間)大于當(dāng)前請求時(shí)間戳
if currentTime + timeRequest > nowTime then
    -- 判斷當(dāng)前時(shí)間窗口請求內(nèi)是否超過限流最大請求數(shù)
    if currentRequest + 1 > maxRequest then
        -- 在時(shí)間窗口內(nèi)且超過限流最大請求數(shù)痴颊,返回
        return 0;
    else
        -- 在時(shí)間窗口內(nèi)且請求數(shù)沒超赏迟,請求數(shù)加一
        redis.call("INCRBY", requestKey, 1)
        return currentRequest + 1;
    end
else
    -- 超時(shí)后重置,開啟一個(gè)新的時(shí)間窗口
    redis.call('set', timeKey, nowTime)
    redis.call('set', requestKey, '0')
    -- 設(shè)置過期時(shí)間
    redis.call("EXPIRE", timeKey, timeRequest / 1000)
    redis.call("EXPIRE", requestKey, timeRequest / 1000)
    -- 請求數(shù)加一
    redis.call("INCRBY", requestKey, 1)
    return 1;
end

2.2. 工具類

  • RedisLimitUtil
package com.example.util;

import ...;

/**
 * RedisLimitUtil
 *
 * @author wliduo[i@dolyw.com]
 * @date 2019/11/14 16:44
 */
@Component
public class RedisLimitUtil {

    /**
     * logger
     */
    private static final Logger logger = LoggerFactory.getLogger(RedisLimitUtil.class);

    /**
     * 秒級限流(每秒限制多少請求)字符串腳本
     */
    private static String LIMIT_SECKILL_SCRIPT = null;

    /**
     * 自定義參數(shù)限流(自定義多少時(shí)間限制多少請求)字符串腳本
     */
    private static String LIMIT_CUSTOM_SCRIPT = null;

    /**
     * redis-key-前綴-limit-限流
     */
    private static final String LIMIT = "limit:";

    /**
     * redis-key-名稱-limit-一個(gè)時(shí)間窗口內(nèi)請求的數(shù)量累計(jì)(限流累計(jì)請求數(shù))
     */
    private static final String LIMIT_REQUEST = "limit:request";

    /**
     * redis-key-名稱-limit-一個(gè)時(shí)間窗口開始時(shí)間(限流開始時(shí)間)
     */
    private static final String LIMIT_TIME = "limit:time";

    /**
     * 構(gòu)造方法初始化加載Lua腳本
     */
    public RedisLimitUtil() {
        LIMIT_SECKILL_SCRIPT = getScript("redis/limit-seckill.lua");
        LIMIT_CUSTOM_SCRIPT = getScript("redis/limit-custom.lua");
    }

    /**
     * 秒級限流判斷(每秒限制多少請求)
     *
     * @param maxRequest 限流最大請求數(shù)
     * @return boolean
     * @throws
     * @author wliduo[i@dolyw.com]
     * @date 2019/11/25 17:57
     */
    public Long limit(String maxRequest) {
        // 獲取key名蠢棱,當(dāng)前時(shí)間戳
        String key = LIMIT + String.valueOf(System.currentTimeMillis() / 1000);
        // 傳入?yún)?shù)锌杀,限流最大請求數(shù)
        List<String> args = new ArrayList<>();
        args.add(maxRequest);
        return eval(LIMIT_SECKILL_SCRIPT, Collections.singletonList(key), args);
    }

    /**
     * 自定義參數(shù)限流判斷(自定義多少時(shí)間限制多少請求)
     *
     * @param maxRequest 限流最大請求數(shù)
     * @param timeRequest 一個(gè)時(shí)間窗口(秒)
     * @return boolean
     * @throws
     * @author wliduo[i@dolyw.com]
     * @date 2019/11/25 17:57
     */
    public Long limit(String maxRequest, String timeRequest) {
        // 獲取key名甩栈,一個(gè)時(shí)間窗口開始時(shí)間(限流開始時(shí)間)和一個(gè)時(shí)間窗口內(nèi)請求的數(shù)量累計(jì)(限流累計(jì)請求數(shù))
        List<String> keys = new ArrayList<>();
        keys.add(LIMIT_TIME);
        keys.add(LIMIT_REQUEST);
        // 傳入?yún)?shù),限流最大請求數(shù)糕再,當(dāng)前時(shí)間戳量没,一個(gè)時(shí)間窗口時(shí)間(毫秒)(限流時(shí)間)
        List<String> args = new ArrayList<>();
        args.add(maxRequest);
        args.add(String.valueOf(System.currentTimeMillis()));
        args.add(timeRequest);
        return eval(LIMIT_CUSTOM_SCRIPT, keys, args);
    }

    /**
     * 執(zhí)行Lua腳本方法
     *
     * @param script
     * @param keys
     * @param args
     * @return java.lang.Object
     * @throws
     * @author wliduo[i@dolyw.com]
     * @date 2019/11/26 10:50
     */
    private Long eval(String script, List<String> keys, List<String> args) {
        // 執(zhí)行腳本
        Object result = JedisUtil.eval(script, keys, args);
        // 結(jié)果請求數(shù)大于0說明不被限流
        return (Long) result;
    }

    /**
     * 獲取Lua腳本
     *
     * @param path
     * @return java.lang.String
     * @throws
     * @author wliduo[i@dolyw.com]
     * @date 2019/11/25 17:57
     */
    private static String getScript(String path) {
        StringBuilder stringBuilder = new StringBuilder();
        InputStream inputStream = RedisLimitUtil.class.getClassLoader().getResourceAsStream(path);
        try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream))) {
            String str;
            while ((str = bufferedReader.readLine()) != null) {
                stringBuilder.append(str).append(System.lineSeparator());
            }
        } catch (IOException e) {
            logger.error(Arrays.toString(e.getStackTrace()));
            throw new CustomException("獲取Lua限流腳本出現(xiàn)問題: " + Arrays.toString(e.getStackTrace()));
        }
        return stringBuilder.toString();
    }

}

2.3. 注解

  • pom.xml(注解借助AOP實(shí)現(xiàn))
<!-- AOP -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
  • Limit
package com.example.limit;

import java.lang.annotation.*;

/**
 * 限流注解
 *
 * @author wliduo[i@dolyw.com]
 * @date 2019/11/26 9:59
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Limit {

    /**
     * 限流最大請求數(shù)
     * @return
     */
    String maxRequest() default "10";

    /**
     * 一個(gè)時(shí)間窗口(毫秒)
     * @return
     */
    String timeRequest() default "1000";

}
  • LimitAspect
package com.example.limit;

import ...;

/**
 * LimitAspect限流切面
 *
 * @author wliduo[i@dolyw.com]
 * @date 2019/11/26 10:07
 */
@Order(0)
@Aspect
@Component
public class LimitAspect {

    /**
     * logger
     */
    private static final Logger logger = LoggerFactory.getLogger(LimitAspect.class);

    /**
     * 一個(gè)時(shí)間窗口時(shí)間(毫秒)(限流時(shí)間)
     */
    private static final String TIME_REQUEST = "1000";

    /**
     * RedisLimitUtil
     */
    @Autowired
    private RedisLimitUtil redisLimitUtil;

    /**
     * 對應(yīng)注解
     *
     * @param
     * @return void
     * @throws
     * @author wliduo[i@dolyw.com]
     * @date 2019/11/26 10:11
     */
    @Pointcut("@annotation(com.example.limit.Limit)")
    public void aspect() {}

    /**
     * 切面
     *
     * @param proceedingJoinPoint
     * @return java.lang.Object
     * @throws
     * @author wliduo[i@dolyw.com]
     * @date 2019/11/26 10:11
     */
    @Around("aspect() && @annotation(limit)")
    public Object Interceptor(ProceedingJoinPoint proceedingJoinPoint, Limit limit) {
        Object result = null;
        Long maxRequest = 0L;
        // 一個(gè)時(shí)間窗口(毫秒)為1000的話默認(rèn)調(diào)用秒級限流判斷(每秒限制多少請求)
        if (TIME_REQUEST.equals(limit.timeRequest())) {
            maxRequest = redisLimitUtil.limit(limit.maxRequest());
        } else {
            maxRequest = redisLimitUtil.limit(limit.maxRequest(), limit.timeRequest());
        }
        // 返回請求數(shù)量大于0說明不被限流
        if (maxRequest > 0) {
            // 放行,執(zhí)行后續(xù)方法
            try {
                result = proceedingJoinPoint.proceed();
            } catch (Throwable throwable) {
                throw new CustomException(throwable.getMessage());
            }
        } else {
            // 直接返回響應(yīng)結(jié)果
            throw new CustomException("請求擁擠突想,請稍候重試");
        }
        return result;
    }

    /**
     * 執(zhí)行方法前再執(zhí)行
     *
     * @param limit
     * @return void
     * @throws
     * @author wliduo[i@dolyw.com]
     * @date 2019/11/26 10:10
     */
    @Before("aspect() && @annotation(limit)")
    public void before(Limit limit) {
        // logger.info("before");
    }

    /**
     * 執(zhí)行方法后再執(zhí)行
     *
     * @param limit
     * @return void
     * @throws
     * @author wliduo[i@dolyw.com]
     * @date 2019/11/26 10:10
     */
    @After("aspect() && @annotation(limit)")
    public void after(Limit limit) {
        // logger.info("after");
    }

}

2.4. 測試入口

寫個(gè) LimitController 簡單測試下殴蹄,工具類和注解的使用,可以使用 PostMan 或者 JMeter 測試猾担,都是 Get 請求饶套,也可以直接用瀏覽器窗口打開請求

package com.example.controller;

import ...;

/**
 *  計(jì)數(shù)器(固定時(shí)間窗口)限流接口測試
 *
 * @author wliduo[i@dolyw.com]
 * @date 2019/11/24 19:27
 */
@RestController
@RequestMapping("/limit")
public class LimitController {

    /**
     * logger
     */
    private static final Logger logger = LoggerFactory.getLogger(LimitController.class);

    /**
     * 一個(gè)時(shí)間窗口內(nèi)最大請求數(shù)(限流最大請求數(shù))
     */
    private static final Long MAX_NUM_REQUEST = 2L;

    /**
     * 一個(gè)時(shí)間窗口時(shí)間(毫秒)(限流時(shí)間)
     */
    private static final Long TIME_REQUEST = 5000L;

    /**
     * 一個(gè)時(shí)間窗口內(nèi)請求的數(shù)量累計(jì)(限流請求數(shù)累計(jì))
     */
    private AtomicInteger requestNum = new AtomicInteger(0);

    /**
     * 一個(gè)時(shí)間窗口開始時(shí)間(限流開始時(shí)間)
     */
    private AtomicLong requestTime = new AtomicLong(System.currentTimeMillis());

    /**
     * RedisLimitUtil
     */
    @Autowired
    private RedisLimitUtil redisLimitUtil;

    /**
     * 計(jì)數(shù)器(固定時(shí)間窗口)請求接口
     *
     * @param
     * @return java.lang.String
     * @throws
     * @author wliduo[i@dolyw.com]
     * @date 2019/11/25 16:19
     */
    @GetMapping
    public String index() {
        long nowTime = System.currentTimeMillis();
        // 判斷是在當(dāng)前時(shí)間窗口(限流開始時(shí)間)
        if (nowTime < requestTime.longValue() + TIME_REQUEST) {
            // 判斷當(dāng)前時(shí)間窗口請求內(nèi)是否限流最大請求數(shù)
            if (requestNum.longValue() < MAX_NUM_REQUEST) {
                // 在時(shí)間窗口內(nèi)且請求數(shù)量還沒超過最大,請求數(shù)加一
                requestNum.incrementAndGet();
                logger.info("請求成功垒探,當(dāng)前請求是{}次", requestNum.intValue());
                return "請求成功妓蛮,當(dāng)前請求是" + requestNum.intValue() + "次";
            }
        } else {
            // 超時(shí)后重置(開啟一個(gè)新的時(shí)間窗口)
            requestTime = new AtomicLong(nowTime);
            requestNum = new AtomicInteger(0);
        }
        logger.info("請求失敗,被限流");
        return "請求失敗圾叼,被限流";
    }

    /**
     * 計(jì)數(shù)器(固定時(shí)間窗口)請求接口(限流工具類實(shí)現(xiàn))
     *
     * @param
     * @return java.lang.String
     * @throws
     * @author wliduo[i@dolyw.com]
     * @date 2019/11/25 18:02
     */
    @GetMapping("/redis")
    public String redis() {
        Long maxRequest = redisLimitUtil.limit(MAX_NUM_REQUEST.toString());
        // 結(jié)果請求數(shù)大于0說明不被限流
        if (maxRequest > 0) {
            logger.info("請求成功蛤克,當(dāng)前請求是{}次", maxRequest);
            return "請求成功,當(dāng)前請求是" + maxRequest + "次";
        }
        logger.info("請求失敗夷蚊,被限流");
        return "請求擁擠构挤,請稍候重試";
    }

    /**
     * 計(jì)數(shù)器(固定時(shí)間窗口)請求接口(限流注解實(shí)現(xiàn))
     *
     * @param
     * @return java.lang.String
     * @throws
     * @author wliduo[i@dolyw.com]
     * @date 2019/11/26 9:46
     */
    @Limit(maxRequest = "2", timeRequest = "3000")
    @GetMapping("/annotation")
    public String annotation() {
        logger.info("請求成功");
        return "請求成功";
    }

}

3. 代碼實(shí)現(xiàn)

有了上面的注解,我們只需要 Controller 加個(gè)方法就行惕鼓,在 SeckillEvolutionController 添加樂觀鎖加緩存再加限流下單的入口方法

  • SeckillEvolutionController
/**
 * 使用樂觀鎖下訂單筋现,并且添加讀緩存,再添加限流
 *
 * @param id 商品ID
 * @return com.example.common.ResponseBean
 * @throws Exception
 * @author wliduo[i@dolyw.com]
 * @date 2019/11/22 14:24
 */
@Limit
@PostMapping("/createOptimisticLockOrderWithRedisLimit/{id}")
public ResponseBean createOptimisticLockOrderWithRedisLimit(@PathVariable("id") Integer id) throws Exception {
    // 錯(cuò)誤的箱歧,線程不安全
    // Integer orderCount = seckillEvolutionService.createOptimisticLockOrderWithRedisWrong(id);
    // 正確的矾飞,線程安全
    Integer orderCount = seckillEvolutionService.createOptimisticLockOrderWithRedisSafe(id);
    return new ResponseBean(HttpStatus.OK.value(), "購買成功", null);
}

添加注解 @Limit 即可,默認(rèn)限流為每秒最多請求10次

4. 開始測試

使用 JMeter 測試上面的代碼呀邢,JMeter 的使用可以查看: JMeter的安裝使用

我們調(diào)用一下商品庫存初始化的方法洒沦,我使用的是 PostMan,初始化庫存表商品 10 個(gè)庫存价淌,而且清空訂單表

圖片

接著使用 PostMan 調(diào)用緩存預(yù)熱方法申眼,提前加載好緩存

圖片

這時(shí)候可以看到我們的數(shù)據(jù),庫存為 10蝉衣,賣出為 0 括尸,訂單表為空

圖片

緩存數(shù)據(jù)也是這樣

圖片

打開 JMeter,添加測試計(jì)劃(測試計(jì)劃文件在項(xiàng)目的src\main\resources\jmx下)病毡,模擬 500 個(gè)并發(fā)線程測試秒殺 10 個(gè)庫存的商品

圖片

PS: 這次我們填寫 Ramp-Up 時(shí)間為 5 秒濒翻,意思為執(zhí)行 5 秒,每秒執(zhí)行 100 個(gè)并發(fā)剪验,因?yàn)槿绻荚?1S 內(nèi)執(zhí)行完肴焊,會被限流前联,然后填寫請求地址,點(diǎn)擊啟動圖標(biāo)開始

圖片

可以看到 500 個(gè)并發(fā)線程執(zhí)行完娶眷,數(shù)據(jù)是正確的

圖片

我們可以看下 Druid 的監(jiān)控似嗤,地址: http://localhost:8080/druid/sql.html

圖片

使用了限流,可以看到樂觀鎖更新不像之前那樣執(zhí)行 157 次了届宠,只執(zhí)行了 36 次烁落,很多請求直接被限流了,我們看下后臺日志豌注,可以看到很多請求直接被限流限制了伤塌,這樣就達(dá)到了我們的目的

圖片

5. 最后總結(jié)

那我們還可以怎么優(yōu)化提高吞吐量以及性能呢,我們上文所有例子其實(shí)都是同步請求轧铁,完全可以利用同步轉(zhuǎn)異步來提高性能每聪,這里我們將下訂單的操作進(jìn)行異步化,利用消息隊(duì)列來進(jìn)行解耦齿风,這樣可以然 DB 異步執(zhí)行下單

每當(dāng)一個(gè)請求通過了限流和庫存校驗(yàn)之后就將訂單信息發(fā)給消息隊(duì)列药薯,這樣一個(gè)請求就可以直接返回了,消費(fèi)程序做下訂單的操作救斑,對數(shù)據(jù)進(jìn)行入庫落地童本,因?yàn)楫惒搅耍宰罱K需要采取回調(diào)或者是其他提醒的方式提醒用戶購買完成

參考

  1. 感謝hllcve_的Spring Boot自定義注解: http://www.reibang.com/p/e04eeae86cf9
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末脸候,一起剝皮案震驚了整個(gè)濱河市穷娱,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌运沦,老刑警劉巖泵额,帶你破解...
    沈念sama閱讀 219,366評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異茶袒,居然都是意外死亡梯刚,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,521評論 3 395
  • 文/潘曉璐 我一進(jìn)店門薪寓,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人澜共,你說我怎么就攤上這事向叉。” “怎么了嗦董?”我有些...
    開封第一講書人閱讀 165,689評論 0 356
  • 文/不壞的土叔 我叫張陵母谎,是天一觀的道長。 經(jīng)常有香客問我京革,道長奇唤,這世上最難降的妖魔是什么幸斥? 我笑而不...
    開封第一講書人閱讀 58,925評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮咬扇,結(jié)果婚禮上甲葬,老公的妹妹穿的比我還像新娘。我一直安慰自己懈贺,他們只是感情好经窖,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,942評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著梭灿,像睡著了一般画侣。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上堡妒,一...
    開封第一講書人閱讀 51,727評論 1 305
  • 那天配乱,我揣著相機(jī)與錄音,去河邊找鬼皮迟。 笑死宪卿,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的万栅。 我是一名探鬼主播佑钾,決...
    沈念sama閱讀 40,447評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼烦粒!你這毒婦竟也來了休溶?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,349評論 0 276
  • 序言:老撾萬榮一對情侶失蹤扰她,失蹤者是張志新(化名)和其女友劉穎兽掰,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體徒役,經(jīng)...
    沈念sama閱讀 45,820評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡孽尽,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,990評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了忧勿。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片杉女。...
    茶點(diǎn)故事閱讀 40,127評論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖鸳吸,靈堂內(nèi)的尸體忽然破棺而出熏挎,到底是詐尸還是另有隱情,我是刑警寧澤晌砾,帶...
    沈念sama閱讀 35,812評論 5 346
  • 正文 年R本政府宣布坎拐,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏哼勇。R本人自食惡果不足惜都伪,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,471評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望积担。 院中可真熱鬧陨晶,春花似錦、人聲如沸磅轻。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,017評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽聋溜。三九已至谆膳,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間撮躁,已是汗流浹背漱病。 一陣腳步聲響...
    開封第一講書人閱讀 33,142評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留把曼,地道東北人杨帽。 一個(gè)月前我還...
    沈念sama閱讀 48,388評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像嗤军,于是被迫代替她去往敵國和親注盈。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,066評論 2 355