Ratelimitfilter利用令牌实现限流

背景:项目中提供接口给第三方平台使用。由于需要针对每个租户做请求并发控制;已知springcloud gateway整合了Redis 使用令牌桶算法做限流算法;参考

Spring Cloud Gateway实战案例(限流、熔断回退、跨域、统一异常处理和重试机制)-腾讯云开发者社区-腾讯云 (tencent.com)
核心算法对象RedisRateLimiter ,原本计划是重写这个对象,把自己的限流规则重新写入这个对象里面,交于springcloud 逻辑中,但是发现自己的重新对象一直无法注册到bean 容器中;就放弃了重新的方案,从而采用自己的的过滤器

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 +
                    '}';
        }
    }
}

单元测试 验证限流效果

    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("限流结果:{}", JSON.toJSONString(results));
            // .log("redisratelimiter", Level.FINER);
            boolean allowed = results.get(0) == 1L;
            Long tokensLeft = results.get(1);

        if(!allowed){
            log.info("请求过于频繁,请稍后在请求");
            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 过滤器

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请求
        if (null != contentType && HttpMethod.POST.name().equalsIgnoreCase(method)) {

            ServerRequest serverRequest = new DefaultServerRequest(exchange);
            // 读取请求体
            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("限流结果:{}", JSON.toJSONString(results));
        // .log("redisratelimiter", Level.FINER);
        boolean allowed = results.get(0) == 1L;
        Long tokensLeft = results.get(1);

        Assert.isTrue(allowed, "请求过于频繁,请稍后在请求");
    }

    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("限流结果:{}", JSON.toJSONString(results));
            // .log("redisratelimiter", Level.FINER);
            boolean allowed = results.get(0) == 1L;
            Long tokensLeft = results.get(1);

            if (!allowed) {
                log.info("请求过于频繁,请稍后在请求");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            ;
            count++;
        }
    }

    /**
     * 获取请求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();
    }


    /**
     * 返回结果
     *
     * @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 {

    /**
     * 多个租户限流规则集合
     */
    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 {


    /**
     * 每个接口 配置的限速
     */
    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;
    /**
     * 桶令牌个数  入30
     */
    private String burstCapacity;


}

每个租户存储的限流规则

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

推荐阅读更多精彩内容