基于Redis+Lua實現分布式限流組件

limit-spring-boot-starter

limit-spring-boot-starter是一個基于springboot starter機制,結合SPI 接口設計思想(內部集成:Redis+Lua實現限流算法(令牌桶,固定窗口,滑動窗口)以及限流回退默認實現),支持注解方式/配置文件方式接入限流,擴展方便,集成使用簡單的分布式限流組件。
開源地址:https://gitee.com/javacoo/limit-spring-boot-starter

背景介紹

業務背景

1、隨著業務的快速發展,對接的第三方合作機構越來越多,對外提供服務API訪問量成倍增加,導致服務器壓力也不斷增加,而服務器資源是有限的,當請求量達到設計的極限時,如果不采取措施,輕則導致服務響應時間變長,重則可能造成整個系統癱瘓。

生產環境背景

1、賬單日批量業務接口訪問量暴增,特別是某個時間段
2、業務方調用接口的速度未知,QPS可能達到400/s,600/s,或者更高
3、對外服務API性能上限是 QPS 300/s
4、已經出現服務不可用,應用崩潰的事故

需求分析

1、鑒于業務方對接口的調用頻率未知,而我方的接口服務有上限,為保證服務的可用性,業務層需要對接口調用方的流量進行限制—–接口限流。

2、盡量少改或者不改造已有功能:少侵入或者0侵入式開發。

3、擴展方便,集成簡單,開發速率高,使用簡單。

設計思路

主流思路
  • 在開發高并發系統時有三把利器用來保護系統:緩存、降級和限流。緩存的目的是提升系統訪問速度和增大系統能處理的容量,可謂是抗高并發流量的銀彈;而降級是當服務出問題或者影響到核心流程的性能則需要暫時屏蔽掉,待高峰或者問題解決后再打開;而有些場景并不能用緩存和降級來解決,比如稀缺資源(秒殺、搶購)、寫服務(如評論、下單)、頻繁的復雜查詢(評論的最后幾頁),因此需有一種手段來限制這些場景的并發/請求量,即限流。

  • 常見的限流有:限制總并發數(比如數據庫連接池、線程池)、限制瞬時并發數(如nginx的limit_conn模塊,用來限制瞬時并發連接數)、限制時間窗口內的平均速率(如Guava的RateLimiter、nginx的limit_req模塊,限制每秒的平均速率);其他還有如限制遠程接口調用速率、限制MQ的消費速率。另外還可以根據網絡連接數、網絡流量、CPU或內存負載等來限流。

  • 如果是單節點我們可以使用google為我們提供的guava包下的RateLimiter進行限流,它使用的是令牌桶算法,分布式場景下也可以使用網關進行限流,如Spring Clound Gateway,其實還有很多開源的限流框架如阿里的Sentinel,甚至我們可以利用redis+lua腳本自己來實現限流。

前面的話

在實際應用時也不要太糾結算法問題,因為一些限流算法實現是一樣的只是描述不一樣;具體使用哪種限流技術還是要根據實際場景來選擇,不要一味去找最佳模式,白貓黑貓能解決問題的就是好貓 :)

我的思路

組件基于springboot starter機制,結合SPI 接口設計思想(內部集成:Redis+Lua實現限流算法(令牌桶,固定窗口,滑動窗口)以及限流回退默認實現),支持注解方式/配置文件方式接入限流,主要分為以下部分:

  • 新建 springboot starter工程
  • 基于SPI思想設計擴展接口
  • redis+lua實現分布式限流
  • 支持接口添加注解方式限流
  • 支持配置文件方式限流
  • 支持限流回退
  • ...

具體實現見 實施步驟 一節,由于實施步驟較多,故放在后面章節,我們先來看看如何集成及使用。

集成及使用

集成
<dependency>
   <groupId>com.javacoo</groupId>
   <artifactId>limit-spring-boot-starter</artifactId>
   <version>1.0.0</version>
</dependency>
使用
  • 配置文件接入限流

    主要配置說明,詳見 LimitConfig 限流配置,覆蓋原則:有方法級獨立配置則使用獨立配置,否則使用全局配置,當配置了 limit.expression限流表達式則激活了配置文件接入限流,注解方式失效。

    config.png
#全局配置
#限流表達式:攔截 com.javacoo.service.example.service 包及子包下所有方法
limit.expression=execution(* com.javacoo.service.example.service..*.*(..))
#全局限流回退實現名稱
limit.limit-rule.fallback-impl=globalFallback

#方法級配置:針對getExampleInfo方法 配置獨立的限流規則
#給定的時間范圍 單位(秒)
limit.limit-method-map[getExampleInfo].period = 60
#一定時間內最多訪問次數
limit.limit-method-map[getExampleInfo].count = 5
#限流類型
limit.limit-method-map[getExampleInfo].limitType = CUSTOMER
#降級策略
limit.limit-method-map[getExampleInfo].fallbackStrategy = FALLBACK
#當降級策略為:回退 時回退處理接口實現名稱
limit.limit-method-map[getExampleInfo].fallbackImpl = getExampleInfoFallback
  • 使用注解方式接入限流:默認配置情況

        //60秒內,允許訪問1次
        @Limit(period = 60,count = 1)
      public Optional<ExampleDto> getExampleInfo(String id) {
          AbstractAssert.isNotBlank(id, ErrorCodeConstants.SERVICE_GET_EXAMPLE_INFO_ID);
            ExampleDto exampleDto = new ExampleDto();
            exampleDto.setId("1");
            exampleDto.setData("正常數據");
            return Optional.ofNullable(exampleDto);
      }
    

    限流效果:正常訪問

    2021-05-29 15:13:38.553  INFO 16452 --- [           main] c.j.l.c.i.redis.RedisLuaRateLimiter      : [限流交易請求],key:[COM.JAVACOO.SERVICE.EXAMPLE.SERVICE.IMPL.EXAMPLESERVICEIMPL.GETEXAMPLEINFO],60秒內,已訪問次數:1,60秒內,限制次數:1
    2021-05-29 15:13:38.553  INFO 16452 --- [           main] c.j.l.c.handler.AbstractLimitHandler     : [限流交易請求],嘗試獲取執行權限成功,開始執行目標方法
    ...
    

    限流效果:降級策略:FAIL_FAST

    2021-05-29 15:14:14.413  INFO 16192 --- [           main] c.j.l.c.i.redis.RedisLuaRateLimiter      : [限流交易請求],key:[COM.JAVACOO.SERVICE.EXAMPLE.SERVICE.IMPL.EXAMPLESERVICEIMPL.GETEXAMPLEINFO],60秒內,已訪問次數:2,60秒內,限制次數:1
    2021-05-29 15:14:14.414  INFO 16192 --- [           main] c.j.l.c.handler.AbstractLimitHandler     : [限流交易請求],嘗試獲取執行權限失敗,服務降級處理,降級策略:FAIL_FAST
    2021-05-29 15:14:14.416 ERROR 16192 --- [           main] c.j.l.c.handler.AbstractLimitHandler     : [限流服務執行異常]
    com.javacoo.limit.client.exception.LimitException: 訪問過于頻繁,超出訪問限制
    ...
    
  • 使用注解接入限流:回退策略情況

    //60秒內,允許訪問1次,回退策略,指定回退處理類
    @Limit(period = 60,count = 1,fallbackStrategy = FallbackStrategy.FALLBACK,fallbackImpl = "getExampleInfoFallback")
    public Optional<ExampleDto> getExampleInfo(String id) {
          AbstractAssert.isNotBlank(id, ErrorCodeConstants.SERVICE_GET_EXAMPLE_INFO_ID);
            ExampleDto exampleDto = new ExampleDto();
            exampleDto.setId("1");
            exampleDto.setData("正常數據");
            return Optional.ofNullable(exampleDto);
      }
    

    限流效果:正常訪問

    2021-05-29 15:22:44.613  INFO 16720 --- [           main] c.j.l.c.i.redis.RedisLuaRateLimiter      : [限流交易請求],key:[COM.JAVACOO.SERVICE.EXAMPLE.SERVICE.IMPL.EXAMPLESERVICEIMPL.GETEXAMPLEINFO],60秒內,已訪問次數:1,60秒內,限制次數:1
    2021-05-29 15:22:44.614  INFO 16720 --- [           main] c.j.l.c.handler.AbstractLimitHandler     : [限流交易請求],嘗試獲取執行權限成功,開始執行目標方法
    ...
    
     限流效果:降級策略:FALLBACK
    
2021-05-29 15:23:09.497  INFO 8592 --- [           main] c.j.l.c.handler.AnnotationLimitHandler   : [AnnotationLimitHandler限流交易請求],嘗試獲取方法:com.javacoo.service.example.service.impl.ExampleServiceImpl.getExampleInfo,執行權限
2021-05-29 15:23:09.666  INFO 8592 --- [           main] c.j.l.c.i.redis.RedisLuaRateLimiter      : [限流交易請求],key:[COM.JAVACOO.SERVICE.EXAMPLE.SERVICE.IMPL.EXAMPLESERVICEIMPL.GETEXAMPLEINFO],60秒內,已訪問次數:2,60秒內,限制次數:1
2021-05-29 15:23:09.666  INFO 8592 --- [           main] c.j.l.c.handler.AbstractLimitHandler     : [限流交易請求],嘗試獲取執行權限失敗,服務降級處理,降級策略:FALLBACK
2021-05-29 15:23:09.668  INFO 8592 --- [           main] c.j.s.e.fallback.GetExampleInfoFallback  : getExampleInfo方法降級處理
...
  • 限流回退擴展實現

    基于xkernel 提供的SPI機制(詳見:https://gitee.com/javacoo/xkernel
    ),擴展非常方便,大致步驟如下:

    1. 實現限流回退接口:如 com.javacoo.service.example.fallback.GetExampleInfoFallback,com.javacoo.service.example.fallback.GlobalFallback

    2. 配置限流回退接口:

      • 在項目resource目錄新建包->META-INF->services

      • 創建com.javacoo.limit.client.api.Fallback文件,文件內容:實現類的全局限定名,如:

        globalFallback=com.javacoo.service.example.fallback.GlobalFallback
        getExampleInfoFallback=com.javacoo.service.example.fallback.GetExampleInfoFallback
        
      fallback.png
 - 修改配置文件,添加如下內容:

   ```properties
   #全局配置
   limit.limit-rule.fallback-impl=globalFallback
   #方法級配置
   limit.limit-method-map[getExampleInfo].fallbackImpl = getExampleInfoFallback
   ```

全局實現

/**
 * 全局方法降級處理接口實現
 * <li></li>
 *
 * @author: duanyong@jccfc.com
 * @since: 2021/5/29 14:35
 */
@Slf4j
public class GlobalFallback implements Fallback<Object> {
    /**
     * 服務降級處理
     * <li></li>
     *
     * @author duanyong@jccfc.com
     * @date 2021/5/29 10:25
     * @return: R 返回對象
     */
    @Override
    public Object getFallback() {
        log.info("全局降級處理");
        return null;
    }
}

指定方法實現

/**
 * getExampleInfo方法降級處理接口實現
 * <li></li>
 *
 * @author: duanyong@jccfc.com
 * @since: 2021/5/29 11:20
 */
@Slf4j
public class GetExampleInfoFallback implements Fallback<Optional<ExampleDto>> {
    /**
     * 服務降級處理
     * <li></li>
     *
     * @author duanyong@jccfc.com
     * @date 2021/5/29 10:25
     * @return: java.lang.Object 返回對象
     */
    @Override
    public Optional<ExampleDto> getFallback() {
        log.info("getExampleInfo方法降級處理");
        ExampleDto exampleDto = new ExampleDto();
        exampleDto.setData("請求過多,請稍后再試");
        return Optional.ofNullable(exampleDto);
    }
}

實施步驟

1,新建limit-spring-boot-starter工程
  • 工程結構
limit.png
  • 類結構圖
RateLimiter.png
  • 項目結構

    limit-spring-boot-starter
     └── src
        ├── main  
        │ ├── java  
        │ │   └── com.javacoo
        │ │   ├────── limit
        │ │   │         ├──────client
        │   │   │         │         ├── api
        │   │   │         │         │    ├── Fallback 服務降級回退處理接口
        │   │   │         │         │    └── RateLimiter 限流接口
        │   │   │         │         ├── annotation
        │   │   │         │         │    └── Limit 限流注解
        │   │   │         │         ├── config
        │   │   │         │         │    ├── LimitRule 限流規則配置
        │   │   │         │         │    └── LimitConfig 限流配置
        │   │   │         │         ├── enums
        │   │   │         │         │    ├── FallbackStrategy 降級策略
        │   │   │         │         │    └── LimitType 限流類型
        │   │   │         │         ├── exception
        │   │   │         │         │    └── LimitException 限流異常
        │   │   │         │         ├── util
        │   │   │         │         │    └── WebUtil 工具類
        │   │   │         │         ├── handler
        │   │   │         │         │    ├── AbstractLimitHandler 抽象限流處理器
        │   │   │         │         │    ├── AnnotationLimitHandler 限流注解處理器
        │   │   │         │         │    ├── ConfigLimitHandler 限流配置處理器
        │   │   │         │         │    └── LimitPointcutAdvisor 限流切面Advisor
        │   │   │         │         └── internal 接口內部實現
        │   │   │         │              ├── redis
        │   │   │         │              │    ├── DefaultFallback 服務降級回退處理接口默認實現
        │   │   │         │              └── redis
        │   │   │         │                   ├── LimitRedisConfig RedisTemplate配置類
        │   │   │         │                   ├── AbstractRateLimiter 抽象限流接口實現
        │   │   │         │                   ├── FixedWindowRateLimiter 固定窗口算法實現類
        │   │   │         │                   ├── LeakyBucketRateLimiter 漏桶算法實現類
        │   │   │         │                   ├── SlidingWindowRateLimiter 滑動窗口算法實現類
        │   │   │         │                   └── TokenBucketRateLimiter 令牌桶算法實現類
        │ │   │         └──────starter
        │   │   │                   ├── LimitAutoConfiguration 自動配置類
        │   │   │                   └── RateLimiterHolder 分布式限流接口對象持有者
        │ └── resource  
        │     ├── META-INF
        │       │      ├── spring.factories
        │       │      └── ext
        │     │           └── internal
        │     │                   ├── com.javacoo.limit.client.api.Fallback
        │     │                   └── com.javacoo.limit.client.api.RateLimiter
        │     └── script
        │             ├── FixedWindow.lua
        │             ├── LeakyBucket.lua
        │             ├── SlidingWindow.lua
        │             └── TokenBucket.lua
        └── test  測試
    
2,基于SPI思想設計擴展接口
  • 限流接口->com.javacoo.limit.client.api.RateLimiter

    /**
     * 限流接口
     * <p>說明:</p>
     * <li></li>
     * @author duanyong
     * @date 2021/5/6 22:25
     */
    @Spi(LimitConfig.DEFAULT_IMPL)
    public interface RateLimiter {
        /** 默認時間單位:秒 */
        TimeUnit DEFAULT_TIME_UNIT = TimeUnit.SECONDS;
        /** 默認限流時間范圍:1 */
        int DEFAULT_PERIOD = 1;
        /** 默認限流數量: 10 */
        int DEFAULT_LIMIT_COUNT = 10;
        /**
         * 嘗試獲取
         * <p>說明:</p>
         * <li></li>
         * @author duanyong
         * @date 2021/5/6 23:09
         * @param keys key
         * @param count 限制數量
         * @return boolean 是否成功
         */
        boolean tryAcquire(ImmutableList<String> keys ,int count);
        /**
         * 嘗試獲取
         * <p>說明:</p>
         * <li></li>
         * @author duanyong
         * @date 2021/5/7 23:09
         * @param keys key
         * @param count 限制數量
         * @param period 時間周期
         * @return boolean 是否成功
         */
        boolean tryAcquire(ImmutableList<String> keys ,int count,int period);
        /**
         * 嘗試獲取
         * <p>說明:</p>
         * <li></li>
         * @author duanyong
         * @date 2021/5/7 23:09
         * @param keys key
         * @param count 限制數量
         * @param period 時間周期
         * @param timeUnit 時間周期單位
         * @return boolean 是否成功
         */
        boolean tryAcquire(ImmutableList<String> keys ,int count,int period,TimeUnit timeUnit);
    }
    
  • 服務降級回退處理接口->com.javacoo.limit.client.api.Fallback

    /**
     * 服務降級回退處理接口
     * <li></li>
     *
     * @author: duanyong@jccfc.com
     * @since: 2021/5/29 10:20
     */
    @Spi(LimitConfig.DEFAULT_IMPL)
    public interface Fallback<R> {
        /**
         * 服務降級處理
         * <li></li>
         * @author duanyong@jccfc.com
         * @date 2021/5/29 10:25
         * @return: R 返回對象
         */
        default R getFallback(){ return null;}
    }
    
3,redis+lua實現分布式限流(內部擴展實現)
  • RedisTemplate配置類

    /**
     * RedisTemplate配置類
     * <li></li>
     *
     * @author: duanyong
     * @since: 2020/6/22 10:41
     */
    @Slf4j
    @Configuration
    public class LimitRedisConfig {
        private static final String DATA_FORMAT = "yyyy-MM-dd HH:mm:ss";
        @Bean
        public RedisTemplate<String, Serializable> limitRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
            RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate<>();
            redisTemplate.setConnectionFactory(redisConnectionFactory);
            FastJsonRedisSerializer<Serializable> fastJsonRedisSerializer = new FastJsonRedisSerializer(Serializable.class);
            FastJsonConfig fastJsonConfig = new FastJsonConfig();
            fastJsonConfig.setDateFormat(DATA_FORMAT);
            fastJsonRedisSerializer.setFastJsonConfig(fastJsonConfig);
            redisTemplate.setValueSerializer(fastJsonRedisSerializer);
            redisTemplate.setHashValueSerializer(fastJsonRedisSerializer);
            // 設置鍵(key)的序列化采用StringRedisSerializer。
            redisTemplate.setKeySerializer(new StringRedisSerializer());
            redisTemplate.setHashKeySerializer(new StringRedisSerializer());
            redisTemplate.afterPropertiesSet();
            return redisTemplate;
        }
    }
    
  • 抽象限流接口實現

    /**
     * 抽象限流接口實現
     * <li></li>
     *
     * @author: duanyong@jccfc.com
     * @since: 2021/5/29 9:35
     */
    @Slf4j
    public abstract class AbstractRateLimiter implements RateLimiter {
        /**
         * RedisScript
         */
        protected RedisScript<Long> redisLuaScript;
        /**
         * RedisTemplate
         */
        @Autowired
        protected RedisTemplate<String, Serializable> redisTemplate;
        @PostConstruct
        public void initLUA() {
            redisLuaScript = new DefaultRedisScript<>(buildLuaScript(), Long.class);
        }
    
        /**
         * 嘗試獲取
         * <p>說明:</p>
         * <li></li>
         *
         * @param keys key
         * @param count 限制數量
         * @return boolean 是否成功
         * @author duanyong
         * @date 2021/5/6 23:09
         */
        @Override
        public boolean tryAcquire(ImmutableList<String> keys , int count) {
            return tryAcquire(keys,count,RateLimiter.DEFAULT_PERIOD);
        }
    
        /**
         * 嘗試獲取
         * <p>說明:</p>
         * <li></li>
         *
         * @param keys key
         * @param count  限制數量
         * @param period 時間周期
         * @return boolean 是否成功
         * @author duanyong
         * @date 2021/5/7 23:09
         */
        @Override
        public boolean tryAcquire(ImmutableList<String> keys ,int count, int period) {
            return tryAcquire(keys,count,period,RateLimiter.DEFAULT_TIME_UNIT);
        }
    
        /**
         * 嘗試獲取
         * <p>說明:</p>
         * <li></li>
         *
         * @param keys key
         * @param count    限制數量
         * @param period   時間周期
         * @param timeUnit 時間周期單位
         * @return boolean 是否成功
         * @author duanyong
         * @date 2021/5/7 23:09
         */
        @Override
        public boolean tryAcquire(ImmutableList<String> keys ,int count, int period, TimeUnit timeUnit) {
            return acquire(keys,count,period,timeUnit);
        }
        /**
         * 構建lua腳本
         * <li></li>
         * @author duanyong@jccfc.com
         * @date 2021/5/29 9:43
         * @return: java.lang.String
         */
        protected abstract String buildLuaScript();
        /**
         * 嘗試獲取
         * <p>說明:</p>
         * <li></li>
         * @author duanyong
         * @date 2021/5/7 22:21
         *
         * @param keys key
         * @param limitCount    限制數量
         * @param limitPeriod   時間周期
         * @param timeUnit 時間周期單位
         * @return boolean 是否成功
         */
        protected abstract boolean acquire(ImmutableList<String> keys ,int limitCount, int limitPeriod, TimeUnit timeUnit);
        /**
         * 加載lua腳本
         * <li></li>
         * @author duanyong@jccfc.com
         * @date 2021/5/29 10:47
         * @param path:
         * @return: java.lang.String
         */
        protected String loadLuaScript(String path){
            try {
                PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(this.getClass().getClassLoader());
                Resource[] resource = resolver.getResources(path);
                String luaScriptContent = StreamUtils.copyToString(resource[0].getInputStream(), StandardCharsets.UTF_8);
                log.info("完成加載Lua腳本:{}",luaScriptContent);
                return luaScriptContent;
            } catch (IOException ioException) {
                ioException.printStackTrace();
                throw new LimitException("加載Lua腳本異常",ioException);
            }
        }
    }
    
  • 固定窗口算法實現類

    **
     * 分布式限流 固定窗口算法實現類
     * <p>說明:</p>
     * <li>固定窗口,一般來說,如非時間緊迫,不建議選擇這個方案,太過生硬。但是,為了能快速止損眼前的問題可以作為臨時應急的方案</li>
     * @author duanyong
     * @date 2021/5/6 22:49
     */
    @Slf4j
    public class FixedWindowRateLimiter extends AbstractRateLimiter {
        /**
         * 構建lua腳本
         * <li></li>
         * @author duanyong@jccfc.com
         * @date 2021/5/29 9:43
         * @return: java.lang.String
         */
        @Override
        protected String buildLuaScript(){
            return loadLuaScript( "classpath:script/FixedWindow.lua");
        }
        /**
         * 嘗試獲取
         * <p>說明:</p>
         * <li></li>
         * @author duanyong
         * @date 2021/5/7 22:21
         *
         * @param keys key
         * @param limitCount    限制數量
         * @param limitPeriod   時間周期
         * @param timeUnit 時間周期單位
         * @return boolean 是否成功
         */
        @Override
        protected boolean acquire(ImmutableList<String> keys ,int limitCount, int limitPeriod, TimeUnit timeUnit){
            Long count = redisTemplate.execute(redisLuaScript, keys,limitCount,limitPeriod);
            log.info("[固定窗口限流交易請求],key:{},返回:{},{}秒內,限制次數:{}",keys,count,limitPeriod,limitCount);
            if (count != null && count.intValue() == 1) {
                return true;
            } else {
                return false;
            }
        }
    }
    

    FixedWindow.lua

    --限流KEY
    local key = KEYS[1]
    --限流大小
    local limit = tonumber(ARGV[1])
    --時間周期
    local period = tonumber(ARGV[2])
    
    local current = tonumber(redis.call('get', key) or "0")
    --如果超出限流大小
    if current + 1 > limit then
        return 0
    else
        redis.call("incr", key)
        if current == 1 then
            redis.call("expire", key,period)
        end
        return 1
    end
    
  • 漏桶算法實現類

  • 滑動窗口算法實現類

    /**
     * 分布式限流 滑動窗口算法實現類
     * <p>說明:</p>
     * <li>滑動窗口。這個方案適用于對異常結果「高容忍」的場景,畢竟相比“兩窗”少了一個緩沖區。但是,勝在實現簡單</li>
     * @author duanyong
     * @date 2021/5/29 22:49
     */
    @Slf4j
    public class SlidingWindowRateLimiter extends AbstractRateLimiter {
        /**
         * 構建lua腳本
         * <li></li>
         * @author duanyong@jccfc.com
         * @date 2021/5/29 9:43
         * @return: java.lang.String
         */
        @Override
        protected String buildLuaScript(){
            return loadLuaScript( "classpath:script/SlidingWindow.lua");
        }
        /**
         * 嘗試獲取
         * <p>說明:</p>
         * <li></li>
         * @author duanyong
         * @date 2021/5/7 22:21
         *
         * @param keys key
         * @param limitCount    限制數量
         * @param limitPeriod   時間周期
         * @param timeUnit 時間周期單位
         * @return boolean 是否成功
         */
        @Override
        protected boolean acquire(ImmutableList<String> keys ,int limitCount, int limitPeriod, TimeUnit timeUnit){
            Long count = redisTemplate.execute(redisLuaScript, keys,"1",limitCount,limitPeriod);
            log.info("[滑動窗口限流交易請求],key:{},{}秒內,返回數量:{},{}秒內,限制次數:{}",keys,limitPeriod,count,limitPeriod,limitCount);
            if (count != null && count.intValue() > 0) {
                return true;
            } else {
                return false;
            }
        }
    }
    

    SlidingWindow.lua

    local function addToQueue(x,time)
        local count=0
        for i=1,x,1 do
            redis.call('lpush',KEYS[1],time)
            count=count+1
        end
        return count
    end
    --返回
    local result=0
    --限流KEY
    local key = KEYS[1]
    --申請數
    local applyCount = tonumber(ARGV[1])
    --閥值數量
    local limit = tonumber(ARGV[2])
    --閥值時間
    local period = tonumber(ARGV[3])
    redis.replicate_commands()
    local now = redis.call('time')[1]
    redis.call('SET','now',now);
    --當前時間
    local current_time = now
    
    local timeBase = redis.call('lindex',key, limit - applyCount)
    if (timeBase == false) or (tonumber(current_time) - tonumber(timeBase) > period) then
        result = result + addToQueue(applyCount,tonumber(current_time))
    end
    if (timeBase ~= false) then
        redis.call('ltrim',key,0,limit)
    end
    return result
    
  • 令牌桶算法實現類

    /**
     * 分布式限流 令牌桶算法實現類
     * <p>說明:</p>
     * <li>令牌桶。當你需要盡可能的壓榨程序的性能(此時桶的最大容量必然會大于等于程序的最大并發能力),并且所處的場景流量進入波動不是很大(不至于一瞬間取完令牌,壓垮后端系統)</li>
     * @author duanyong
     * @date 2021/5/29 22:49
     */
    @Slf4j
    public class TokenBucketRateLimiter extends AbstractRateLimiter {
        /**
         * 構建lua腳本
         * <li></li>
         * @author duanyong@jccfc.com
         * @date 2021/5/29 9:43
         * @return: java.lang.String
         */
        @Override
        protected String buildLuaScript(){
            return loadLuaScript( "classpath:script/TokenBucket.lua");
        }
        /**
         * 嘗試獲取
         * <p>說明:</p>
         * <li></li>
         * @author duanyong
         * @date 2021/5/7 22:21
         *
         * @param keys key
         * @param limitCount    限制數量
         * @param limitPeriod   時間周期
         * @param timeUnit 時間周期單位
         * @return boolean 是否成功
         */
        @Override
        protected boolean acquire(ImmutableList<String> keys ,int limitCount, int limitPeriod, TimeUnit timeUnit){
            Long count = redisTemplate.execute(redisLuaScript, keys,limitCount,limitPeriod);
            log.info("[令牌桶限流交易請求],key:{},返回:{},{}秒內,限制次數:{}",keys,count,limitPeriod,limitCount);
            if (count != null && count.intValue() == 1) {
                return true;
            } else {
                return false;
            }
        }
    }
    

    TokenBucket.lua

    --利用redis的hash結構,存儲key所對應令牌桶的上次獲取時間和上次獲取后桶中令牌數量
    local ratelimit_info = redis.pcall('HMGET',KEYS[1],'last_time','current_token_num')
    local last_time = ratelimit_info[1]
    local current_token_num = tonumber(ratelimit_info[2])
    
    redis.replicate_commands()
    local now = redis.call('time')[1]
    redis.call('SET','now',now);
    
    
    
    --tonumber是將value轉換為數字,此步是取出桶中最大令牌數、生成令牌的速率(每秒生成多少個)、當前時間
    local max_token_num = tonumber(ARGV[1])
    local token_rate = tonumber(ARGV[2])
    --local current_time = tonumber(ARGV[3])
    local current_time = now
    --reverse_time 即多少毫秒生成一個令牌
    local reverse_time = 1000/token_rate
    
    --如果current_token_num不存在則說明令牌桶首次獲取或已過期,即說明它是滿的
    if current_token_num == nil then
      current_token_num = max_token_num
      last_time = current_time
    else
      --計算出距上次獲取已過去多長時間
      local past_time = current_time-last_time
      --在這一段時間內可產生多少令牌
      local reverse_token_num = math.floor(past_time/reverse_time)
      current_token_num = current_token_num +reverse_token_num
      last_time = reverse_time * reverse_token_num + last_time
      if current_token_num > max_token_num then
        current_token_num = max_token_num
      end
    end
    
    local result = 0
    if(current_token_num > 0) then
      result = 1
      current_token_num = current_token_num - 1
    end
    
    --將最新得出的令牌獲取時間和當前令牌數量進行存儲,并設置過期時間
    redis.call('HMSET',KEYS[1],'last_time',last_time,'current_token_num',current_token_num)
    redis.call('pexpire',KEYS[1],math.ceil(reverse_time*(max_token_num - current_token_num)+(current_time-last_time)))
    
    return result
    
4,支持接口添加注解方式限流
  • 自定義限流注解

    /**
     * 自定義限流注解
     * <p>說明:</p>
     * <li></li>
     *
     * @author duanyong
     * @date 2021/5/6 21:47
     */
    @Target({ElementType.METHOD, ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Inherited
    @Documented
    public @interface Limit {
        /**
         * 名字
         */
        String name() default "";
    
        /**
         * key
         */
        String key() default "";
    
        /**
         * Key的前綴
         */
        String prefix() default "";
    
        /**
         * 給定的時間范圍 單位(秒)
         */
        int period();
    
        /**
         * 一定時間內最多訪問次數
         */
        int count();
    
        /**
         * 限流的類型(用戶自定義key 或者 請求ip)
         */
        LimitType limitType() default LimitType.CUSTOMER;
    
        /**
         * 降級策略:默認快速失敗
         */
        FallbackStrategy fallbackStrategy() default FallbackStrategy.FAIL_FAST;
    
        /**
         * 當降級策略為:回退 時回退處理接口實現名稱
         */
        String fallbackImpl() default "";
    }
    
  • 限流注解處理器

    /**
     * 限流注解處理器
     * <li>基于aspectj</li>
     *
     * @author: duanyong@jccfc.com
     * @since: 2021/5/29 17:04
     */
    @Slf4j
    @Aspect
    @Component
    public class AnnotationLimitHandler extends AbstractLimitHandler<Object>{
    
        @Around("@annotation(limit)")
        public Object around(ProceedingJoinPoint joinPoint, Limit limit) throws Throwable {
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            // 獲取被攔截的方法
            Method method = signature.getMethod();
            // 獲取被攔截的類名
            String className = signature.getDeclaringType().getName();
            // 獲取被攔截的方法名
            String methodName = method.getName();
            String defaultKey = StringUtils.join(className,".", methodName);
            log.info("[AnnotationLimitHandler限流交易請求],嘗試獲取方法:{},執行權限", defaultKey);
            //組裝限流規則
            LimitRule limitRule = LimitRule.builder()
                .defaultKey(defaultKey)
                .count(limit.count())
                .key(limit.key())
                .limitType(limit.limitType())
                .period(limit.period())
                .prefix(limit.prefix())
                .fallbackStrategy(limit.fallbackStrategy())
                .fallbackImpl(limit.fallbackImpl())
                .build();
            return handle(limitRule, () -> {
                try {
                    return joinPoint.proceed();
                } catch (Throwable throwable) {
                    log.error("[AnnotationLimitHandler限流交易請求],方法執行異常",throwable);
                }
                return null;
            });
        }
    }
    
5,支持配置文件方式限流
  • 接口限流配置

    /**
     * 接口限流配置
     * @author duanyong
     * @date 2021/3/4 15:15
     */
    @ConfigurationProperties(prefix = LimitConfig.PREFIX)
    public class LimitConfig {
        /** 前綴 */
        public static final String PREFIX = "limit";
        /** 前綴 */
        public static final String EXP = "expression";
        /** limit是否可用,默認值*/
        public static final String ENABLED = "enabled";
        /** 默認實現,默認值*/
        public static final String DEFAULT_IMPL= "default";
        /** PointCut表達式,默認值*/
        public static final String DEFAULT_EXP= "";
        /** limit是否可用*/
        private String enabled = ENABLED;
        /**實現*/
        private String impl = DEFAULT_IMPL;
        /**PointCut表達式*/
        private String expression = DEFAULT_EXP;
        /**全局限流規則配置*/
        @NestedConfigurationProperty
        private LimitRule limitRule = new LimitRule().init();
        /**特殊接口限流規則配置Map*/
        private Map<String, LimitRule> limitMethodMap = new HashMap<>(5);
    
        public String getEnabled() {
            return enabled;
        }
    
        public void setEnabled(String enabled) {
            this.enabled = enabled;
        }
    
        public String getImpl() {
            return impl;
        }
    
        public void setImpl(String impl) {
            this.impl = impl;
        }
    
        public String getExpression() {
            return expression;
        }
    
        public void setExpression(String expression) {
            this.expression = expression;
        }
    
        public LimitRule getLimitRule() {
            return limitRule;
        }
    
        public void setLimitRule(LimitRule limitRule) {
            this.limitRule = limitRule;
        }
    
        public Map<String, LimitRule> getLimitMethodMap() {
            return limitMethodMap;
        }
    
        public void setLimitMethodMap(Map<String, LimitRule> limitMethodMap) {
            this.limitMethodMap = limitMethodMap;
        }
    }
    
  • 限流規則配置

    /**
     * 限流規則配置
     * <li>單個接口方法限流規則配置</li>
     *
     * @author: duanyong@jccfc.com
     * @since: 2021/5/29 15:31
     */
    @Data
    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
    public class LimitRule {
        /** 給定的時間范圍 單位(秒),默認值*/
        public static final int DEFAULT_PERIOD = 1;
        /** 一定時間內最多訪問次數,默認值*/
        public static final int DEFAULT_COUNT = 50;
        /**
         * defaultKey
         */
        private String defaultKey = "";
        /**
         * key
         */
        private String key;
    
        /**
         * Key的前綴
         */
        private String prefix;
        /**
         * 給定的時間范圍 單位(秒)
         */
        private int period;
    
        /**
         * 一定時間內最多訪問次數
         */
        private int count;
        /**
         * 限流類型
         */
        private LimitType limitType;
        /**
         * 降級策略:默認快速失敗
         */
        private FallbackStrategy fallbackStrategy;
        /**
         * 當降級策略為:回退 時回退處理接口實現名稱
         */
        private String fallbackImpl;
        /**
         * 初始化
         * <li></li>
         * @author duanyong@jccfc.com
         * @date 2021/5/29 14:06
         * @return: void
         */
        public LimitRule init(){
            this.setPeriod(DEFAULT_PERIOD);
            this.setCount(DEFAULT_COUNT);
            this.setLimitType(LimitType.CUSTOMER);
            this.setFallbackStrategy(FallbackStrategy.FAIL_FAST);
            this.setFallbackImpl(LimitConfig.DEFAULT_IMPL);
            return this;
        }
    }
    
  • 限流配置處理器

    /**
     * 限流配置處理器
     * <li>處理配置方式限流</li>
     *
     * @author: duanyong@jccfc.com
     * @since: 2021/5/29 17:06
     */
    @Slf4j
    public class ConfigLimitHandler extends AbstractLimitHandler<Object> implements MethodInterceptor {
        @Nullable
        @Override
        public Object invoke(@Nonnull MethodInvocation methodInvocation) throws Throwable {
            // 獲取被攔截的類名
            String className = methodInvocation.getClass().getName();
            // 獲取被攔截的方法名
            String methodName = methodInvocation.getMethod().getName();
            String defaultKey = StringUtils.join(className,".", methodName);
            //獲取配置文件中的限流規則
            LimitRule limitRule = lockConfig.getLimitRule();
            //如果需要特殊處理
            if(lockConfig.getLimitMethodMap().containsKey(methodName)){
                limitRule = lockConfig.getLimitMethodMap().get(methodName);
            }
            limitRule.setDefaultKey(defaultKey);
            log.info("[ConfigLimitHandler限流交易請求],嘗試獲取方法:{},執行權限", defaultKey);
            return handle(limitRule, () -> {
                try {
                    return methodInvocation.proceed();
                } catch (Throwable throwable) {
                    log.error("[ConfigLimitHandler限流交易請求],方法執行異常",throwable);
                }
                return null;
            });
        }
    }
    
  • 限流切面Advisor

    /**
     * 限流切面Advisor
     * <li></li>
     *
     * @author: duanyong@jccfc.com
     * @since: 2021/5/29 10:52
     */
    @Slf4j
    public class LimitPointcutAdvisor extends AbstractBeanFactoryPointcutAdvisor {
        @Autowired
        private LimitConfig lockConfig;
        @Override
        public Pointcut getPointcut() {
            AspectJExpressionPointcut adapterPointcut = new AspectJExpressionPointcut();
            //從配置文件中獲取PointCut表達式
            adapterPointcut.setExpression(lockConfig.getExpression());
            return adapterPointcut;
        }
    }
    
6,支持限流回退
  • 服務降級回退處理接口默認實現

    /**
     * 服務降級回退處理接口默認實現
     * <li></li>
     *
     * @author: duanyong@jccfc.com
     * @since: 2021/5/29 11:00
     */
    public class DefaultFallback implements Fallback<Object> {
        /**
         * 服務降級處理
         * <li></li>
         *
         * @author duanyong@jccfc.com
         * @date 2021/5/29 10:25
         * @return: java.lang.Object 返回對象
         */
        @Override
        public Object getFallback() {
            return null;
        }
    }
    
7,其他輔助類
  • 降級策略枚舉類

    /**
     * 降級策略
     * <li></li>
     *
     * @author: duanyong@jccfc.com
     * @since: 2021/5/29 10:32
     */
    public enum FallbackStrategy {
        /**
         * 快速失敗
         */
        FAIL_FAST,
        /**
         * 回退
         */
        FALLBACK;
    }
    
  • 限流類型枚舉類

    /**
     * 限流類型
     * <p>說明:</p>
     * <li></li>
     *
     * @author duanyong
     * @date 2021/5/6 21:47
     */
    public enum LimitType {
        /**
         * 自定義key
         */
        CUSTOMER,
    
        /**
         * 請求者IP
         */
        IP;
    }
    
  • 限流異常類

    /**
     * 限流異常
     * <li></li>
     *
     * @author: duanyong
     * @since: 2021/4/29 11:20
     */
    public class LimitException extends RuntimeException{
        /**錯誤碼*/
        protected String code;
        public LimitException() {
        }
    
        public LimitException(Throwable ex) {
            super(ex);
        }
        public LimitException(String message) {
            super(message);
        }
        public LimitException(String code, String message) {
            super(message);
            this.code = code;
        }
        public LimitException(String message, Throwable ex) {
            super(message, ex);
        }
    }
    
  • 抽象限流處理器

    /**
     * 抽象限流處理器
     * <p>說明:</p>
     * <li></li>
     *
     * @author duanyong@jccfc.com
     * @date 2021/5/29 21:48
     */
    @Slf4j
    public abstract class AbstractLimitHandler<R> {
        @Autowired
        protected LimitConfig lockConfig;
        @Autowired
        private RateLimiter rateLimiter;
    
        /**
         * 限流處理
         * <li></li>
         * @author duanyong@jccfc.com
         * @date 2021/5/29 9:34
         * @param limitRule: 限流規則
         * @param function: 結果提供函數
         * @return: R
         */
        protected R handle(LimitRule limitRule, Supplier<R> function){
            //規則整理
            handleLimitRule(limitRule);
    
            ImmutableList<String> keys = ImmutableList.of(StringUtils.join(limitRule.getPrefix(), limitRule.getKey()));
            try {
                if (rateLimiter.tryAcquire(keys,limitRule.getCount(), limitRule.getPeriod())) {
                    log.info("[限流交易請求],嘗試獲取執行權限成功,開始執行目標方法");
                    return function.get();
                }else{
                    log.info("[限流交易請求],嘗試獲取執行權限失敗,服務降級處理,降級策略:{}",limitRule.getFallbackStrategy());
                    if(FallbackStrategy.FAIL_FAST.equals(limitRule.getFallbackStrategy())){
                        throw new LimitException("訪問過于頻繁,超出訪問限制");
                    }
                    //獲取回退處理接口實現:如果為空則使用默認實現
                    Fallback<R> fallback = ExtensionLoader.getExtensionLoader(Fallback.class).getExtension(limitRule.getFallbackImpl());
                    fallback = fallback != null ? fallback : ExtensionLoader.getExtensionLoader(Fallback.class).getDefaultExtension();
                    return fallback.getFallback();
                }
            } catch (Throwable e) {
                log.error("[限流服務執行異常]",e);
                if (e instanceof LimitException) {
                    throw e;
                }
                throw new LimitException("限流服務執行異常",e);
            }
        }
        /**
         * 限流規則整理
         * <li></li>
         * @author duanyong@jccfc.com
         * @date 2021/5/29 14:15
         * @param limitRule: 限流規則
         * @return: void
         */
        private void handleLimitRule(LimitRule limitRule){
            //限流規則整理:如果為空則使用全局配置
            limitRule.setKey(StringUtils.isBlank(limitRule.getKey()) ? lockConfig.getLimitRule().getKey() : limitRule.getKey());
            limitRule.setCount(limitRule.getCount() == 0 ? lockConfig.getLimitRule().getCount() : limitRule.getCount());
            limitRule.setFallbackImpl(StringUtils.isBlank(limitRule.getFallbackImpl()) ? lockConfig.getLimitRule().getFallbackImpl() : limitRule.getFallbackImpl());
            limitRule.setFallbackStrategy(limitRule.getFallbackStrategy() == null ? lockConfig.getLimitRule().getFallbackStrategy() : limitRule.getFallbackStrategy());
            limitRule.setLimitType(limitRule.getLimitType() == null ? lockConfig.getLimitRule().getLimitType() : limitRule.getLimitType());
            limitRule.setPeriod(limitRule.getPeriod() == 0 ? lockConfig.getLimitRule().getPeriod() : limitRule.getPeriod());
            limitRule.setPrefix(StringUtils.isBlank(limitRule.getPrefix()) ? lockConfig.getLimitRule().getPrefix() : limitRule.getPrefix());
    
            String key = limitRule.getKey();
            //根據限流類型獲取不同的key ,如果不傳我們會以方法名作為key
            switch (limitRule.getLimitType()) {
                case IP:
                    key = WebUtil.getIpAddress();
                    break;
                case CUSTOMER:
                    key = StringUtils.isBlank(key) ? StringUtils.upperCase(limitRule.getDefaultKey()) : key;
                    break;
                default:
                    key = StringUtils.upperCase(limitRule.getDefaultKey());
            }
            limitRule.setKey(key);
        }
    }
    
  • 工具類

    /**
     * 工具類
     * <li></li>
     *
     * @author: duanyong@jccfc.com
     * @since: 2021/5/29 15:58
     */
    public class WebUtil {
        private static final String UNKNOWN = "unknown";
        /**
         * 獲取IP
         * <p>說明:</p>
         * <li></li>
         * @author duanyong
         * @date 2021/5/7 22:35
         */
        public static String getIpAddress() {
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            String ip = request.getHeader("x-forwarded-for");
            if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
                ip = request.getHeader("Proxy-Client-IP");
            }
            if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
                ip = request.getHeader("WL-Proxy-Client-IP");
            }
            if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
                ip = request.getRemoteAddr();
            }
            return ip;
        }
    }
    
  • 自動配置類

    /**
     * 自動配置類
     * <li></li>
     * @author duanyong
     * @date 2021/3/5 9:50
     */
    @Slf4j
    @Configuration
    @EnableConfigurationProperties(value = LimitConfig.class)
    @ConditionalOnClass(RateLimiter.class)
    @ConditionalOnProperty(prefix = LimitConfig.PREFIX, value = LimitConfig.ENABLED, matchIfMissing = true)
    public class LimitAutoConfiguration {
      @Autowired
        private LimitConfig lockConfig;
    
      @Bean
      @ConditionalOnMissingBean(RateLimiter.class)
      public RateLimiter createRateLimiter() {
            log.info("初始化分布式限流對象,實現類名稱:{}",lockConfig.getImpl());
            RateLimiterHolder.rateLimiter = ExtensionLoader.getExtensionLoader(RateLimiter.class).getExtension(lockConfig.getImpl());
            log.info("初始化分布式限流對象成功,實現類:{}", RateLimiterHolder.rateLimiter);
            return RateLimiterHolder.rateLimiter;
      }
        
        @Bean
        @ConditionalOnProperty(prefix = LimitConfig.PREFIX, value = LimitConfig.EXP)
        @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
        public LimitPointcutAdvisor adapterServiceAdvisor() {
            LimitPointcutAdvisor advisor = new LimitPointcutAdvisor();
            advisor.setAdviceBeanName("limitPointcutAdvisor");
            advisor.setAdvice(createConfigLimitHandler());
            advisor.setOrder(Ordered.HIGHEST_PRECEDENCE);
            return advisor;
        }
        @Bean
        public ConfigLimitHandler createConfigLimitHandler() {
            return new ConfigLimitHandler();
        }
    
        @Bean
        @ConditionalOnMissingBean(LimitPointcutAdvisor.class)
        public AnnotationLimitHandler createAnnotationLimitHandler() {
            return new AnnotationLimitHandler();
        }
    }
    
  • 限流接口對象持有者

    /**
     * 限流接口對象持有者
     * <p>說明:</p>
     * <li></li>
     * @author duanyong
     * @date 2021/5/7 22:27
     */
    public class RateLimiterHolder {
        /** 限流對象*/
        static RateLimiter rateLimiter;
    
        public static Optional<RateLimiter> getRateLimiter() {
            return Optional.ofNullable(rateLimiter);
        }
    }
    
8,資源文件
  • spring.factories

    org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
      com.javacoo.limit.client.internal.redis.LimitRedisConfig,\
      com.javacoo.limit.starter.LimitAutoConfiguration
    
    
  • com.javacoo.limit.client.api.Fallback

    default=com.javacoo.limit.client.internal.fallback.DefaultFallback
    
  • com.javacoo.limit.client.api.RateLimiter

    default=com.javacoo.limit.client.internal.redis.TokenBucketRateLimiter
    fixedWindow=com.javacoo.limit.client.internal.redis.FixedWindowRateLimiter
    slidingWindow=com.javacoo.limit.client.internal.redis.SlidingWindowRateLimiter
    

問題及局限性

問題

  • 緊經過小規模生產驗證,雖滿足業務需求,但是還不算成熟,任需更多測試驗證。

  • 僅供學習參考,如需用于生產,請謹慎,并多測試。

  • 推薦使用Spring Clound Gateway,Sentinel等專業流量防護組件

    局限性

  • 限流組件保證了高可用,犧牲了性能,增加了一層 IO 環節的開銷,單機限流在本地,分布式限流還要通過網絡協議。

  • 限流組件保證了高可用,犧牲了一致性,在大流量的情況下,請求的處理會出現延遲的情況,這種場景便無法保證強一致性。特殊情況下,還無法保證最終一致性,部分請求直接被拋棄。

  • 限流組件擁有流控權,若限流組件掛了,會引起雪崩效應,導致請求與業務的大批量失敗。

  • 引入限流組件,增加系統的復雜程度,開發難度增加,限流中間件的設計本身就是一個復雜的體系,需要綜合業務與技術去思考與權衡,同時還要確保限流組件本身的高可用與性能,極大增加工作量,甚至需要一個團隊去專門開發。

一些信息

路漫漫其修遠兮,吾將上下而求索
碼云:https://gitee.com/javacoo
QQ:164863067
作者/微信:javacoo
郵箱:xihuady@126.com
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,837評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,196評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,688評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,654評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,456評論 6 406
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,955評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,044評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,195評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,725評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,608評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,802評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,318評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,048評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,422評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,673評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,424評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,762評論 2 372

推薦閱讀更多精彩內容