Ratelimitfilter利用令牌實(shí)現(xiàn)限流

背景:項(xiàng)目中提供接口給第三方平臺(tái)使用。由于需要針對(duì)每個(gè)租戶做請(qǐng)求并發(fā)控制剧辐;已知springcloud gateway整合了Redis 使用令牌桶算法做限流算法腐宋;參考

Spring Cloud Gateway實(shí)戰(zhàn)案例(限流卫枝、熔斷回退筒溃、跨域迁央、統(tǒng)一異常處理和重試機(jī)制)-騰訊云開(kāi)發(fā)者社區(qū)-騰訊云 (tencent.com)
核心算法對(duì)象RedisRateLimiter ,原本計(jì)劃是重寫(xiě)這個(gè)對(duì)象钙皮,把自己的限流規(guī)則重新寫(xiě)入這個(gè)對(duì)象里面逐抑,交于springcloud 邏輯中,但是發(fā)現(xiàn)自己的重新對(duì)象一直無(wú)法注冊(cè)到bean 容器中汇四;就放棄了重新的方案,從而采用自己的的過(guò)濾器

package org.springframework.cloud.gateway.filter.ratelimit;

import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;

import javax.validation.constraints.Min;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jetbrains.annotations.NotNull;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import org.springframework.beans.BeansException;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.validation.Validator;
import org.springframework.validation.annotation.Validated;

/**
 * See https://stripe.com/blog/rate-limiters and
 * https://gist.github.com/ptarjan/e38f45f2dfe601419ca3af937fff574d#file-1-check_request_rate_limiter-rb-L11-L34
 *
 * @author Spencer Gibb
 */
@ConfigurationProperties("spring.cloud.gateway.redis-rate-limiter")
public class RedisRateLimiter extends AbstractRateLimiter<RedisRateLimiter.Config> implements ApplicationContextAware {
    @Deprecated
    public static final String REPLENISH_RATE_KEY = "replenishRate";
    @Deprecated
    public static final String BURST_CAPACITY_KEY = "burstCapacity";

    public static final String CONFIGURATION_PROPERTY_NAME = "redis-rate-limiter";
    public static final String REDIS_SCRIPT_NAME = "redisRequestRateLimiterScript";
    public static final String REMAINING_HEADER = "X-RateLimit-Remaining";
    public static final String REPLENISH_RATE_HEADER = "X-RateLimit-Replenish-Rate";
    public static final String BURST_CAPACITY_HEADER = "X-RateLimit-Burst-Capacity";

    private Log log = LogFactory.getLog(getClass());

    private ReactiveRedisTemplate<String, String> redisTemplate;
    private RedisScript<List<Long>> script;
    private AtomicBoolean initialized = new AtomicBoolean(false);
    private Config defaultConfig;

    // configuration properties
    /** Whether or not to include headers containing rate limiter information, defaults to true. */
    private boolean includeHeaders = true;

    /** The name of the header that returns number of remaining requests during the current second. */
    private String remainingHeader = REMAINING_HEADER;

    /** The name of the header that returns the replenish rate configuration. */
    private String replenishRateHeader = REPLENISH_RATE_HEADER;

    /** The name of the header that returns the burst capacity configuration. */
    private String burstCapacityHeader = BURST_CAPACITY_HEADER;

    public RedisRateLimiter(ReactiveRedisTemplate<String, String> redisTemplate,
                            RedisScript<List<Long>> script, Validator validator) {
        super(Config.class, CONFIGURATION_PROPERTY_NAME, validator);
        this.redisTemplate = redisTemplate;
        this.script = script;
        initialized.compareAndSet(false, true);
    }

    public RedisRateLimiter(int defaultReplenishRate, int defaultBurstCapacity) {
        super(Config.class, CONFIGURATION_PROPERTY_NAME, null);
        this.defaultConfig = new Config()
                .setReplenishRate(defaultReplenishRate)
                .setBurstCapacity(defaultBurstCapacity);
    }

    public boolean isIncludeHeaders() {
        return includeHeaders;
    }

    public void setIncludeHeaders(boolean includeHeaders) {
        this.includeHeaders = includeHeaders;
    }

    public String getRemainingHeader() {
        return remainingHeader;
    }

    public void setRemainingHeader(String remainingHeader) {
        this.remainingHeader = remainingHeader;
    }

    public String getReplenishRateHeader() {
        return replenishRateHeader;
    }

    public void setReplenishRateHeader(String replenishRateHeader) {
        this.replenishRateHeader = replenishRateHeader;
    }

    public String getBurstCapacityHeader() {
        return burstCapacityHeader;
    }

    public void setBurstCapacityHeader(String burstCapacityHeader) {
        this.burstCapacityHeader = burstCapacityHeader;
    }

    @Override
    @SuppressWarnings("unchecked")
    public void setApplicationContext(ApplicationContext context) throws BeansException {
        if (initialized.compareAndSet(false, true)) {
            this.redisTemplate = context.getBean("stringReactiveRedisTemplate", ReactiveRedisTemplate.class);
            this.script = context.getBean(REDIS_SCRIPT_NAME, RedisScript.class);
            if (context.getBeanNamesForType(Validator.class).length > 0) {
                this.setValidator(context.getBean(Validator.class));
            }
        }
    }

    /* for testing */ Config getDefaultConfig() {
        return defaultConfig;
    }

    /**
     * This uses a basic token bucket algorithm and relies on the fact that Redis scripts
     * execute atomically. No other operations can run between fetching the count and
     * writing the new count.
     */
    @Override
    @SuppressWarnings("unchecked")
    public Mono<Response> isAllowed(String routeId, String id) {
        if (!this.initialized.get()) {
            throw new IllegalStateException("RedisRateLimiter is not initialized");
        }

        Config routeConfig = getConfig().getOrDefault(routeId, defaultConfig);

        if (routeConfig == null) {
            throw new IllegalArgumentException("No Configuration found for route " + routeId);
        }

        // How many requests per second do you want a user to be allowed to do?
        int replenishRate = routeConfig.getReplenishRate();

        // How much bursting do you want to allow?
        int burstCapacity = routeConfig.getBurstCapacity();

        try {
            List<String> keys = getKeys(id);


            // The arguments to the LUA script. time() returns unixtime in seconds.
            List<String> scriptArgs = Arrays.asList(replenishRate + "", burstCapacity + "",
                    Instant.now().getEpochSecond() + "", "1");
            // allowed, tokens_left = redis.eval(SCRIPT, keys, args)
            Flux<List<Long>> flux = this.redisTemplate.execute(this.script, keys, scriptArgs);
                    // .log("redisratelimiter", Level.FINER);
            return flux.onErrorResume(throwable -> Flux.just(Arrays.asList(1L, -1L)))
                    .reduce(new ArrayList<Long>(), (longs, l) -> {
                        longs.addAll(l);
                        return longs;
                    }) .map(results -> {
                        boolean allowed = results.get(0) == 1L;
                        Long tokensLeft = results.get(1);

                        Response response = new Response(allowed, getHeaders(routeConfig, tokensLeft));

                        if (log.isDebugEnabled()) {
                            log.debug("response: " + response);
                        }
                        return response;
                    });
        }
        catch (Exception e) {
            /*
             * We don't want a hard dependency on Redis to allow traffic. Make sure to set
             * an alert so you know if this is happening too much. Stripe's observed
             * failure rate is 0.01%.
             */
            log.error("Error determining if user allowed from redis", e);
        }
        return Mono.just(new Response(true, getHeaders(routeConfig, -1L)));
    }

    @NotNull
    public HashMap<String, String> getHeaders(Config config, Long tokensLeft) {
        HashMap<String, String> headers = new HashMap<>();
        headers.put(this.remainingHeader, tokensLeft.toString());
        headers.put(this.replenishRateHeader, String.valueOf(config.getReplenishRate()));
        headers.put(this.burstCapacityHeader, String.valueOf(config.getBurstCapacity()));
        return headers;
    }

    static List<String> getKeys(String id) {
        // use `{}` around keys to use Redis Key hash tags
        // this allows for using redis cluster

        // Make a unique key per user.
        String prefix = "request_rate_limiter.{" + id;

        // You need two Redis keys for Token Bucket.
        String tokenKey = prefix + "}.tokens";
        String timestampKey = prefix + "}.timestamp";
        return Arrays.asList(tokenKey, timestampKey);
    }

    @Validated
    public static class Config {
        @Min(1)
        private int replenishRate;

        @Min(1)
        private int burstCapacity = 1;

        public int getReplenishRate() {
            return replenishRate;
        }

        public Config setReplenishRate(int replenishRate) {
            this.replenishRate = replenishRate;
            return this;
        }

        public int getBurstCapacity() {
            return burstCapacity;
        }

        public Config setBurstCapacity(int burstCapacity) {
            this.burstCapacity = burstCapacity;
            return this;
        }

        @Override
        public String toString() {
            return "Config{" +
                    "replenishRate=" + replenishRate +
                    ", burstCapacity=" + burstCapacity +
                    '}';
        }
    }
}

單元測(cè)試 驗(yàn)證限流效果

    public static void main(String[] args) {

        RedisTemplate<String, String> template = new RedisTemplate<>();

        RedisStandaloneConfiguration redisClusterConfiguration = new RedisStandaloneConfiguration();
        redisClusterConfiguration.setHostName("10.0.124.60");
        redisClusterConfiguration.setPort(6379);
        LettuceConnectionFactory fac = new LettuceConnectionFactory(redisClusterConfiguration);
        fac.afterPropertiesSet();
        template.setConnectionFactory(fac);
        template.setDefaultSerializer(new StringRedisSerializer());
        template.afterPropertiesSet();

        DefaultRedisScript redisScript = new DefaultRedisScript<>();
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("scripts/request_rate_limiter.lua")));
        redisScript.setResultType(List.class);
        int count = 0;
        while (count < 10) {
            String key = "key";//headerMap.get(Sign.CA_PROXY_SIGN_SECRET_KEY);
            List<String> keys = getKeys(key);
//        Object limitKey = redisTemplate.opsForHash().get("limit_key", key);
//        JSONObject limitKeyJson = JSON.parseObject((String) limitKey);

            String replenishRate = "3";//limitKeyJson.getString("replenishRate");
            String burstCapacity = "2";//limitKeyJson.getString("burstCapacity");

            // The arguments to the LUA script. time() returns unixtime in seconds.
            List<String> scriptArgs = Arrays.asList();
            // allowed, tokens_left = redis.eval(SCRIPT, keys, args)
            List<Long> results = (List<Long>) template.execute(redisScript, keys, replenishRate + "", burstCapacity + "", Instant.now().getEpochSecond() + "", "1");
            log.info("限流結(jié)果:{}", JSON.toJSONString(results));
            // .log("redisratelimiter", Level.FINER);
            boolean allowed = results.get(0) == 1L;
            Long tokensLeft = results.get(1);

        if(!allowed){
            log.info("請(qǐng)求過(guò)于頻繁撕贞,請(qǐng)稍后在請(qǐng)求");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };
        count++;
     }
    }

lua 腳本

local tokens_key = KEYS[1]
local timestamp_key = KEYS[2]
--redis.log(redis.LOG_WARNING, "tokens_key " .. tokens_key)

local rate = tonumber(ARGV[1])
local capacity = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])

local fill_time = capacity/rate
local ttl = math.floor(fill_time*2)

local last_tokens = tonumber(redis.call("get", tokens_key))
if last_tokens == nil then
  last_tokens = capacity
end
--redis.log(redis.LOG_WARNING, "last_tokens " .. last_tokens)

local last_refreshed = tonumber(redis.call("get", timestamp_key))
if last_refreshed == nil then
  last_refreshed = 0
end
--redis.log(redis.LOG_WARNING, "last_refreshed " .. last_refreshed)

local delta = math.max(0, now-last_refreshed)
local filled_tokens = math.min(capacity, last_tokens+(delta*rate))
local allowed = filled_tokens >= requested
local new_tokens = filled_tokens
local allowed_num = 0
if allowed then
  new_tokens = filled_tokens - requested
  allowed_num = 1
end

redis.call("setex", tokens_key, ttl, new_tokens)
redis.call("setex", timestamp_key, ttl, now)

return { allowed_num, new_tokens }

RateLimiterFilter 過(guò)濾器

package com.jdh.opengateway.filter;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.jdh.opengateway.model.cif.RateLimitTenantRule;
import com.jdh.opengateway.model.cif.RateLimitUrlRule;
import com.jdh.opengateway.utils.HttpConstant;
import com.jdh.opengateway.utils.Sign;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.support.BodyInserterContext;
import org.springframework.cloud.gateway.support.CachedBodyOutputMessage;
import org.springframework.cloud.gateway.support.DefaultServerRequest;
import org.springframework.core.Ordered;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.web.reactive.function.BodyInserter;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import javax.annotation.PostConstruct;
import java.net.URI;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;

import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ORIGINAL_REQUEST_URL_ATTR;

@Component
@SuppressWarnings("All")
public class RateLimiterFilter implements GlobalFilter, Ordered {


    private static final Logger log = LoggerFactory.getLogger(RateLimiterFilter.class);

    static DefaultRedisScript<List> redisScript;
    @Autowired
    RedisTemplate redisTemplate;

    @PostConstruct
    public void redisRequestRateLimiterScript() {
        redisScript = new DefaultRedisScript<>();
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("scripts/request_rate_limiter.lua")));
        redisScript.setResultType(List.class);
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();
        String contentType = request.getHeaders().getFirst(HttpConstant.HTTP_HEADER_CONTENT_TYPE);
        String method = request.getMethodValue();
        String url = getUrl(exchange);

        //判斷是否為POST請(qǐng)求
        if (null != contentType && HttpMethod.POST.name().equalsIgnoreCase(method)) {

            ServerRequest serverRequest = new DefaultServerRequest(exchange);
            // 讀取請(qǐng)求體
            Mono<String> modifiedBody = serverRequest.bodyToMono(String.class)
                    .flatMap(body -> {
                        try {
                            reqLimiter(body, url, request);
                        } catch (Exception e) {
                            return Mono.error(e);
                        }
                        return Mono.just(body);
                    });
            return returnMono(exchange, chain, modifiedBody);
        }

        return chain.filter(exchange);
    }

    static List<String> getKeys(String id) {
        // use `{}` around keys to use Redis Key hash tags
        // this allows for using redis cluster
        // Make a unique key per user.
        String prefix = "request_rate_limiter.{" + id;
        // You need two Redis keys for Token Bucket.
        String tokenKey = prefix + "}.tokens";
        String timestampKey = prefix + "}.timestamp";
        return Arrays.asList(tokenKey, timestampKey);
    }

    public void reqLimiter(String body, String url, ServerHttpRequest request) {
        Map<String, String> headerMap = request.getHeaders().toSingleValueMap();
        JSONObject jsonObject = JSON.parseObject(body);
        String key = headerMap.get(Sign.CA_PROXY_SIGN_SECRET_KEY);

        Object limitKey = redisTemplate.opsForHash().get("ratelimitkey", key);
        RateLimitTenantRule rateLimitTenantRule = JSONObject.parseObject((String) limitKey, RateLimitTenantRule.class);

        HashMap<String, RateLimitUrlRule> urlRuleMap = rateLimitTenantRule.getUrlRuleMap();
        RateLimitUrlRule rateLimitUrlRule = urlRuleMap.get(url);

        List<String> keys = getKeys(key);
        String replenishRate = "10";
        String burstCapacity = "10";

        if (rateLimitUrlRule != null) {
            key = key + url;//Redis  key精確到URL
            keys = getKeys(key);
            replenishRate = rateLimitUrlRule.getReplenishRate();
            burstCapacity = rateLimitUrlRule.getBurstCapacity();
        }

        // The arguments to the LUA script. time() returns unixtime in seconds.
        List<String> scriptArgs = Arrays.asList(replenishRate + "", burstCapacity + "", Instant.now().getEpochSecond() + "", "1");
        // allowed, tokens_left = redis.eval(SCRIPT, keys, args)
        List<Long> results = (List<Long>) redisTemplate.execute(redisScript, keys, scriptArgs);
        log.info("限流結(jié)果:{}", JSON.toJSONString(results));
        // .log("redisratelimiter", Level.FINER);
        boolean allowed = results.get(0) == 1L;
        Long tokensLeft = results.get(1);

        Assert.isTrue(allowed, "請(qǐng)求過(guò)于頻繁,請(qǐng)稍后在請(qǐng)求");
    }

    public static void main(String[] args) {

        RedisTemplate<String, String> template = new RedisTemplate<>();

        RedisStandaloneConfiguration redisClusterConfiguration = new RedisStandaloneConfiguration();
        redisClusterConfiguration.setHostName("10.0.124.60");
        redisClusterConfiguration.setPort(6379);
        LettuceConnectionFactory fac = new LettuceConnectionFactory(redisClusterConfiguration);
        fac.afterPropertiesSet();
        template.setConnectionFactory(fac);
        template.setDefaultSerializer(new StringRedisSerializer());
        template.afterPropertiesSet();

        DefaultRedisScript redisScript = new DefaultRedisScript<>();
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("scripts/request_rate_limiter.lua")));
        redisScript.setResultType(List.class);
        int count = 0;
        while (count < 10) {
            String key = "key";//headerMap.get(Sign.CA_PROXY_SIGN_SECRET_KEY);
            List<String> keys = getKeys(key);
//        Object limitKey = redisTemplate.opsForHash().get("limit_key", key);
//        JSONObject limitKeyJson = JSON.parseObject((String) limitKey);

            String replenishRate = "3";//limitKeyJson.getString("replenishRate");
            String burstCapacity = "2";//limitKeyJson.getString("burstCapacity");

            // The arguments to the LUA script. time() returns unixtime in seconds.
            List<String> scriptArgs = Arrays.asList();
            // allowed, tokens_left = redis.eval(SCRIPT, keys, args)
            List<Long> results = (List<Long>) template.execute(redisScript, keys, replenishRate + "", burstCapacity + "", Instant.now().getEpochSecond() + "", "1");
            log.info("限流結(jié)果:{}", JSON.toJSONString(results));
            // .log("redisratelimiter", Level.FINER);
            boolean allowed = results.get(0) == 1L;
            Long tokensLeft = results.get(1);

            if (!allowed) {
                log.info("請(qǐng)求過(guò)于頻繁帆离,請(qǐng)稍后在請(qǐng)求");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            ;
            count++;
        }
    }

    /**
     * 獲取請(qǐng)求URL
     *
     * @param exchange
     * @return
     */
    private String getUrl(ServerWebExchange exchange) {
        LinkedHashSet<URI> uris = exchange.getAttribute(GATEWAY_ORIGINAL_REQUEST_URL_ATTR);
        AtomicReference<String> path = new AtomicReference<>("");
        uris.forEach(uri -> {
            path.set(uri.getPath());
        });
        return path.get();
    }


    /**
     * 返回結(jié)果
     *
     * @param openReq
     * @param exchange
     * @param chain
     * @param modifiedBody
     * @param token
     * @return
     */
    private Mono<Void> returnMono(ServerWebExchange exchange, GatewayFilterChain chain, Mono<String> modifiedBody) {
        BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, String.class);

        CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, exchange.getRequest().getHeaders());
        return bodyInserter.insert(outputMessage, new BodyInserterContext())
                .then(Mono.defer(() -> {
                    ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(exchange.getRequest()) {
                        @Override
                        public Flux<DataBuffer> getBody() {
                            return outputMessage.getBody();
                        }

                        @Override
                        public HttpHeaders getHeaders() {
                            HttpHeaders httpHeaders = new HttpHeaders();
                            httpHeaders.putAll(super.getHeaders());
                            return httpHeaders;
                        }
                    };
                    return chain.filter(exchange.mutate().request(decorator).build());
                }));
    }

    @Override
    public int getOrder() {
        return 3;
    }


}


package com.jdh.opengateway.model.cif;

import lombok.Data;

import java.io.Serializable;
import java.util.HashMap;

@Data
public class RateLimitRule implements Serializable {

    /**
     * 多個(gè)租戶限流規(guī)則集合
     */
    private HashMap<String,RateLimitTenantRule> rateLimitTenantRuleMap;
}

package com.jdh.opengateway.model.cif;

import lombok.Data;

import java.io.Serializable;
import java.util.HashMap;
@Data
public class RateLimitTenantRule  implements Serializable {


    /**
     * 每個(gè)接口 配置的限速
     */
    private HashMap<String,RateLimitUrlRule> urlRuleMap;
}


package com.jdh.opengateway.model.cif;

import lombok.Data;

import java.io.Serializable;
@Data
public class RateLimitUrlRule implements Serializable {

    /**
     * 添加令牌速度 如10/秒
     */
    private String replenishRate;
    /**
     * 桶令牌個(gè)數(shù)  入30
     */
    private String burstCapacity;


}

每個(gè)租戶存儲(chǔ)的限流規(guī)則

{
    "ratelimitkey": {
        "租戶1id": {
            "請(qǐng)求url": {
                "replenishRate": "令牌投遞速度 幾個(gè)每秒",
                "burstCapacity": "令牌個(gè)數(shù)"
            }
        },
        "租戶2id": {
            "請(qǐng)求url": {
                "replenishRate": "令牌投遞速度 幾個(gè)每秒",
                "burstCapacity": "令牌個(gè)數(shù)"
            }
        }
    }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌柬脸,老刑警劉巖鲜结,帶你破解...
    沈念sama閱讀 218,858評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異活逆,居然都是意外死亡精刷,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)蔗候,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)怒允,“玉大人,你說(shuō)我怎么就攤上這事锈遥∪沂拢” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,282評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵所灸,是天一觀的道長(zhǎng)丽惶。 經(jīng)常有香客問(wèn)我,道長(zhǎng)爬立,這世上最難降的妖魔是什么钾唬? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,842評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮侠驯,結(jié)果婚禮上抡秆,老公的妹妹穿的比我還像新娘。我一直安慰自己陵霉,他們只是感情好琅轧,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著踊挠,像睡著了一般乍桂。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上效床,一...
    開(kāi)封第一講書(shū)人閱讀 51,679評(píng)論 1 305
  • 那天睹酌,我揣著相機(jī)與錄音,去河邊找鬼剩檀。 笑死憋沿,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的沪猴。 我是一名探鬼主播辐啄,決...
    沈念sama閱讀 40,406評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼采章,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了壶辜?” 一聲冷哼從身側(cè)響起悯舟,我...
    開(kāi)封第一講書(shū)人閱讀 39,311評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎砸民,沒(méi)想到半個(gè)月后抵怎,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,767評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡岭参,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年反惕,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片演侯。...
    茶點(diǎn)故事閱讀 40,090評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡姿染,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出秒际,到底是詐尸還是另有隱情盔粹,我是刑警寧澤,帶...
    沈念sama閱讀 35,785評(píng)論 5 346
  • 正文 年R本政府宣布程癌,位于F島的核電站,受9級(jí)特大地震影響轴猎,放射性物質(zhì)發(fā)生泄漏嵌莉。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評(píng)論 3 331
  • 文/蒙蒙 一捻脖、第九天 我趴在偏房一處隱蔽的房頂上張望锐峭。 院中可真熱鬧,春花似錦可婶、人聲如沸沿癞。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,988評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)椎扬。三九已至,卻和暖如春具温,著一層夾襖步出監(jiān)牢的瞬間蚕涤,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,101評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工铣猩, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留揖铜,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,298評(píng)論 3 372
  • 正文 我出身青樓达皿,卻偏偏與公主長(zhǎng)得像天吓,于是被迫代替她去往敵國(guó)和親贿肩。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評(píng)論 2 355

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