Spring重試之Spring-Retry,Guava-Retry乡恕,Hystrix言询,F(xiàn)ast-Retry

1 重試之Spring-Retry

1.1 簡(jiǎn)介

Spring RetrySpring框架提供的一個(gè)模塊,它通過提供注解或編程方式的方式傲宜,幫助我們實(shí)現(xiàn)方法級(jí)別的重試機(jī)制运杭。在Spring Boot中,可以很方便地集成并使用Spring Retry

b02168e4a2e20b83e39c61d53767c162_7b8db2586a8540e3a7ab7c3efbe76f78.png

1.2 直接使用

Spring RetrySpring 應(yīng)用程序提供了聲明性重試支持蛋哭。它用于Spring批處理县习、Spring集成、Apache Hadoop(等等)谆趾。它主要是針對(duì)可能拋出異常的一些調(diào)用操作躁愿,進(jìn)行有策略的重試

1.2.1 pom.xml

準(zhǔn)備工作
我們只需要加上依賴:

 <dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
    <version>1.2.2.RELEASE</version>
 </dependency>

1.2.2 重試任務(wù)

準(zhǔn)備一個(gè)任務(wù)方法,這里是采用一個(gè)隨機(jī)整數(shù)沪蓬,根據(jù)不同的條件返回不同的值彤钟,或者拋出異常

package com.zgd.demo.thread.retry;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomUtils;
import org.springframework.remoting.RemoteAccessException;

/**
 * @Description:
 */
@Slf4j
public class RetryDemoTask {


  /**
   * 重試方法
   * @return
   */
  public static boolean retryTask(String param)  {
    log.info("收到請(qǐng)求參數(shù):{}",param);

    int i = RandomUtils.nextInt(0,11);
    log.info("隨機(jī)生成的數(shù):{}",i);
    if (i == 0) {
      log.info("為0,拋出參數(shù)異常.");
      throw new IllegalArgumentException("參數(shù)異常");
    }else if (i  == 1){
      log.info("為1,返回true.");
      return true;
    }else if (i == 2){
      log.info("為2,返回false.");
      return false;
    }else{
      //為其他
        log.info("大于2,拋出自定義異常.");
        throw new RemoteAccessException("大于2,拋出遠(yuǎn)程訪問異常");
      }
    }
}

1.2.3 使用SpringRetryTemplate

這里可以寫我們的代碼了

package com.zgd.demo.thread.retry.spring;

import com.zgd.demo.thread.retry.RetryDemoTask;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.springframework.remoting.RemoteAccessException;
import org.springframework.retry.backoff.FixedBackOffPolicy;
import org.springframework.retry.policy.SimpleRetryPolicy;
import org.springframework.retry.support.RetryTemplate;

import java.util.HashMap;
import java.util.Map;

/**
 * @Description: spring-retry 重試框架
 */
@Slf4j
public class SpringRetryTemplateTest {

  /**
   * 重試間隔時(shí)間ms,默認(rèn)1000ms
   * */
  private long fixedPeriodTime = 1000L;
  /**
   * 最大重試次數(shù),默認(rèn)為3
   */
  private int maxRetryTimes = 3;
  /**
   * 表示哪些異常需要重試,key表示異常的字節(jié)碼,value為true表示需要重試
   */
  private Map<Class<? extends Throwable>, Boolean> exceptionMap = new HashMap<>();


  @Test
  public void test() {
    exceptionMap.put(RemoteAccessException.class,true);

    // 構(gòu)建重試模板實(shí)例
    RetryTemplate retryTemplate = new RetryTemplate();

    // 設(shè)置重試回退操作策略,主要設(shè)置重試間隔時(shí)間
    FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();
    backOffPolicy.setBackOffPeriod(fixedPeriodTime);

    // 設(shè)置重試策略跷叉,主要設(shè)置重試次數(shù)
    SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(maxRetryTimes, exceptionMap);

    retryTemplate.setRetryPolicy(retryPolicy);
    retryTemplate.setBackOffPolicy(backOffPolicy);

    Boolean execute = retryTemplate.execute(
            //RetryCallback
            retryContext -> {
              boolean b = RetryDemoTask.retryTask("abc");
              log.info("調(diào)用的結(jié)果:{}", b);
              return b;
            },
            retryContext -> {
              //RecoveryCallback
              log.info("已達(dá)到最大重試次數(shù)或拋出了不重試的異常~~~");
              return false;
            }
      );

    log.info("執(zhí)行結(jié)果:{}",execute);

  }

}

簡(jiǎn)單剖析下案例代碼逸雹,RetryTemplate 承擔(dān)了重試執(zhí)行者的角色营搅,它可以設(shè)置SimpleRetryPolicy(重試策略,設(shè)置重試上限梆砸,重試的根源實(shí)體)转质,FixedBackOffPolicy(固定的回退策略,設(shè)置執(zhí)行重試回退的時(shí)間間隔)帖世。

RetryTemplate通過execute提交執(zhí)行操作休蟹,需要準(zhǔn)備RetryCallbackRecoveryCallback 兩個(gè)類實(shí)例

  • RetryCallback:對(duì)應(yīng)的就是重試回調(diào)邏輯實(shí)例,包裝正常的功能操作
  • RecoveryCallback:實(shí)現(xiàn)的是整個(gè)執(zhí)行操作結(jié)束的恢復(fù)操作實(shí)例

注意:只有在調(diào)用的時(shí)候拋出了異常日矫,并且異常是在exceptionMap中配置的異常赂弓,才會(huì)執(zhí)行重試操作,否則就調(diào)用到excute方法的第二個(gè)執(zhí)行方法RecoveryCallback

當(dāng)然哪轿,重試策略還有很多種盈魁,回退策略也是:

  • 重試策略
    • NeverRetryPolicy: 只允許調(diào)用RetryCallback一次,不允許重試
    • AlwaysRetryPolicy: 允許無限重試窃诉,直到成功杨耙,此方式邏輯不當(dāng)會(huì)導(dǎo)致死循環(huán)
    • SimpleRetryPolicy: 固定次數(shù)重試策略,默認(rèn)重試最大次數(shù)為3次飘痛,RetryTemplate默認(rèn)使用的策略
    • TimeoutRetryPolicy: 超時(shí)時(shí)間重試策略按脚,默認(rèn)超時(shí)時(shí)間為1秒,在指定的超時(shí)時(shí)間內(nèi)允許重試
    • ExceptionClassifierRetryPolicy: 設(shè)置不同異常的重試策略敦冬,類似組合重試策略辅搬,區(qū)別在于這里只區(qū)分不同異常的重試
    • CircuitBreakerRetryPolicy: 有熔斷功能的重試策略,需設(shè)置3個(gè)參數(shù)openTimeout脖旱、resetTimeoutdelegate
    • CompositeRetryPolicy: 組合重試策略堪遂,有兩種組合方式,樂觀組合重試策略是指只要有一個(gè)策略允許即可以重試萌庆,悲觀組合重試策略是指只要有一個(gè)策略不允許即可以重試溶褪,但不管哪種組合方式,組合中的每一個(gè)策略都會(huì)執(zhí)行
  • 重試回退策略
    重試回退策略践险,指的是每次重試是立即重試還是等待一段時(shí)間后重試猿妈。
    默認(rèn)情況下是立即重試,如果需要配置等待一段時(shí)間后重試則需要指定回退策略BackoffRetryPolicy巍虫。
    • NoBackOffPolicy: 無退避算法策略彭则,每次重試時(shí)立即重試
    • FixedBackOffPolicy: 固定時(shí)間的退避策略,需設(shè)置參數(shù)sleeperbackOffPeriod占遥,sleeper指定等待策略俯抖,默認(rèn)是Thread.sleep,即線程休眠瓦胎,backOffPeriod指定休眠時(shí)間芬萍,默認(rèn)1秒
    • UniformRandomBackOffPolicy: 隨機(jī)時(shí)間退避策略尤揣,需設(shè)置sleeperminBackOffPeriodmaxBackOffPeriod柬祠,該策略在minBackOffPeriod,maxBackOffPeriod之間取一個(gè)隨機(jī)休眠時(shí)間北戏,minBackOffPeriod默認(rèn)500毫秒,maxBackOffPeriod默認(rèn)1500毫秒
    • ExponentialBackOffPolicy: 指數(shù)退避策略漫蛔,需設(shè)置參數(shù)sleeper最欠、initialIntervalmaxIntervalmultiplier惩猫,initialInterval指定初始休眠時(shí)間,默認(rèn)100毫秒蚜点,maxInterval指定最大休眠時(shí)間轧房,默認(rèn)30秒,multiplier指定乘數(shù)绍绘,即下一次休眠時(shí)間為當(dāng)前休眠時(shí)間*multiplier
    • ExponentialRandomBackOffPolicy: 隨機(jī)指數(shù)退避策略奶镶,引入隨機(jī)乘數(shù)可以實(shí)現(xiàn)隨機(jī)乘數(shù)回退

上面的代碼的話,簡(jiǎn)單的設(shè)置了重試間隔為1秒陪拘,重試的異常是RemoteAccessException厂镇,下面就是測(cè)試代碼的情況: 重試第二次成功的情況:

在這里插入圖片描述

重試一次以后,遇到了沒有指出需要重試的異常左刽,直接結(jié)束重試捺信,調(diào)用retryContext


在這里插入圖片描述

重試了三次后,達(dá)到了最大重試次數(shù)欠痴,調(diào)用retryContext


在這里插入圖片描述

1.3 注解使用方式

既然是Spring家族的東西迄靠,那么自然就支持和Spring-Boot整合

1.3.1 注解介紹

我們只要在需要重試的方法上加@Retryable,在重試失敗的回調(diào)方法上加@Recover喇辽,下面是這些注解的屬性

@EnableRetry掌挚,表示是否開啟重試

序號(hào) 屬性 類型 默認(rèn)值 說明
1 proxyTargetClass boolean false 指示是否要?jiǎng)?chuàng)建基于子類的(CGLIB)代理,而不是創(chuàng)建標(biāo)準(zhǔn)的基于Java接口的代理

@Retryable菩咨,標(biāo)注此注解的方法在發(fā)生異常時(shí)會(huì)進(jìn)行重試

序號(hào) 屬性 類型 默認(rèn)值 說明
1 interceptor String "" 將interceptor的bean名稱應(yīng)用到retryable()
2 value Class[] {} 可重試的異常類型
3 label String "" 統(tǒng)計(jì)報(bào)告的唯一標(biāo)簽吠式。如果沒有提供,調(diào)用這可以選擇忽略它抽米,或者提供默認(rèn)值
4 maxAttempts int 3 嘗試的最大次數(shù)(包括第一次失敗)特占,默認(rèn)為3次
5 backoff @Backoff @Backoff() 指定用于重試此操作的backoff屬性。默認(rèn)為空
6 exclude Class[] {} 排除可重試的異常類型

@Backoff

序號(hào) 屬性 類型 默認(rèn)值 說明
1 delay long 0 如果不設(shè)置則默認(rèn)使用 1000 milliseconds 重試等待
2 maxDelay long 0 最大重試等待時(shí)間
3 multiplier long 0 用于計(jì)算下一個(gè)延遲的乘數(shù)(大于0生效)
4 random boolean false 隨機(jī)重試等待時(shí)間

1.3.2 pom.xml

 <dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
    <version>1.2.2.RELEASE</version>
 </dependency>

 <dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.1</version>
 </dependency>

1.3.3 代碼

application啟動(dòng)類上加上@EnableRetry的注解

@EnableRetry
public class Application {
 ...
}

為了方便測(cè)試云茸,這里寫了一個(gè)SpringBootTest的測(cè)試基類摩钙,需要使用SpringBootTest的只要繼承這個(gè)類就好了

package com.zgd.demo.thread.test;

/**
 * @Author: zgd
 * @Description:
 */

import com.zgd.demo.thread.Application;
import lombok.extern.slf4j.Slf4j;
import org.junit.After;
import org.junit.Before;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

/**
 * @Author: zgd
 * @Date: 18/09/29 20:33
 * @Description:
 */
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
@Slf4j
public class MyBaseTest {


  @Before
  public void init() {
    log.info("----------------測(cè)試開始---------------");
  }

  @After
  public void after() {
    log.info("----------------測(cè)試結(jié)束---------------");
  }

}

建一個(gè)service類

package com.zgd.demo.thread.retry.spring;

import com.zgd.demo.thread.retry.RetryDemoTask;
import com.zgd.demo.thread.test.MyBaseTest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.remoting.RemoteAccessException;
import org.springframework.retry.ExhaustedRetryException;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Component;

/**
 * @Author: zgd
 * @Description:
 */
@Service
@Slf4j
public class SpringRetryDemo   {

 /**
   * 重試所調(diào)用方法
   * @param param
   * @return
   */
  @Retryable(value = {RemoteAccessException.class},maxAttempts = 3,backoff = @Backoff(delay = 2000L,multiplier = 2))
  public boolean call(String param){
      return RetryDemoTask.retryTask(param);
  }

  /**
   * 達(dá)到最大重試次數(shù),或拋出了一個(gè)沒有指定進(jìn)行重試的異常
   * recover 機(jī)制
   * @param e 異常
   */
  @Recover
  public boolean recover(Exception e,String param) {
    log.error("達(dá)到最大重試次數(shù),或拋出了一個(gè)沒有指定進(jìn)行重試的異常:",e);
    return false;
  }

}

然后我們調(diào)用這個(gè)service里面的call方法

package com.zgd.demo.thread.retry.spring;

import com.zgd.demo.thread.test.MyBaseTest;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * @Author: zgd
 * @Description:
 */
@Component
@Slf4j
public class SpringRetryDemoTest extends MyBaseTest {

  @Autowired
  private SpringRetryDemo springRetryDemo;

  @Test
  public void retry(){
    boolean abc = springRetryDemo.call("abc");
    log.info("--結(jié)果是:{}--",abc);
  }

}

這里我依然是RemoteAccessException的異常才重試,@Backoff(delay = 2000L,multiplier = 2))表示第一次間隔2秒查辩,以后都是次數(shù)的2倍胖笛,也就是第二次4秒网持,第三次6秒.

1.3.4 測(cè)試結(jié)果

遇到了沒有指定重試的異常,這里指定重試的異常是 @Retryable(value = {RemoteAccessException.class}...,所以拋出參數(shù)異常IllegalArgumentException的時(shí)候长踊,直接回調(diào)@Recover的方法

在這里插入圖片描述

重試達(dá)到最大重試次數(shù)時(shí)功舀,調(diào)用@Recover的方法

在這里插入圖片描述

重試到最后一次沒有報(bào)錯(cuò),返回false


在這里插入圖片描述

1.4 @Retryable和@Recover注解使用問題

@Retryable@RecoverSpring Framework中的注解身弊,用于支持在方法執(zhí)行期間發(fā)生異常時(shí)的重試和恢復(fù)操作辟汰。

  • @Retryable:注解用于標(biāo)記方法,在方法執(zhí)行期間發(fā)生異常時(shí)進(jìn)行重試阱佛。重試行為可以使用Spring Retry框架提供的默認(rèn)策略或自定義策略來定義帖汞。可以指定要重試的異常類型以及最大重試次數(shù)和重試間隔等參數(shù)凑术。
  • @Recover:注解用于標(biāo)記一個(gè)恢復(fù)方法翩蘸,在最終重試失敗后執(zhí)行該方法』囱罚恢復(fù)方法應(yīng)具有與原始方法相同的參數(shù)和返回類型催首,并且應(yīng)在同一類中聲明。如果未指定恢復(fù)方法泄鹏,則重試失敗后將拋出最后一次異常郎任。

1.4.1 @Retryable和@Recover必須定義在一個(gè)類當(dāng)中嗎

@Retryable@Recover注解必須定義在同一個(gè)類中。這是因?yàn)?code>@Retryable注解標(biāo)記的方法需要調(diào)用重試邏輯备籽,而@Recover注解標(biāo)記的方法需要提供重試失敗后的恢復(fù)邏輯舶治。因此,這兩個(gè)方法必須在同一個(gè)類中车猬,以便它們能夠相互調(diào)用歼疮。

例如,以下示例演示了@Retryable@Recover注解在同一個(gè)類中的使用

@Service
public class MyService {
 
    @Retryable(RuntimeException.class)
    public void myMethod() {
        // Some code that may throw a RuntimeException
    }
 
    @Recover
    public void recover() {
        // Recovery logic goes here
    }
}

在上面的示例中诈唬,MyService類中的myMethod()方法使用@Retryable注解進(jìn)行標(biāo)記韩脏,以便在發(fā)生RuntimeException異常時(shí)進(jìn)行重試。如果重試最終失敗铸磅,則recover()方法將被調(diào)用以提供恢復(fù)邏輯赡矢。由于這兩個(gè)方法在同一個(gè)類中定義,因此它們可以相互調(diào)用阅仔。

1.4.2 如果一個(gè)類中有多個(gè)@Recover和@Retryable怎么區(qū)分

如果一個(gè)類中有多個(gè)方法標(biāo)記了@Retryable@Recover注解吹散,你可以通過value屬性來區(qū)分它們。value屬性允許你指定一個(gè)異常類型的數(shù)組八酒,以區(qū)分在方法執(zhí)行期間拋出的不同異常類型空民。

例如,以下示例演示了在同一個(gè)類中有多個(gè)使用@Retryable和@Recover注解的方法的情況:

@Service
public class MyService {
 
    @Retryable(value = {IOException.class})
    public void methodA() throws IOException {
        // Some code that may throw an IOException
    }
 
    @Recover
    public void recoverA(IOException e) {
        // Recovery logic for methodA() goes here
    }
 
    @Retryable(value = {NullPointerException.class})
    public void methodB() throws NullPointerException {
        // Some code that may throw a NullPointerException
    }
 
    @Recover
    public void recoverB(NullPointerException e) {
        // Recovery logic for methodB() goes here
    }
}

在上面的示例中,methodA()和methodB()方法都標(biāo)記為@Retryable注解界轩,以便在拋出IOException或NullPointerException異常時(shí)進(jìn)行重試画饥。然后,對(duì)于每個(gè)方法浊猾,都定義了一個(gè)恢復(fù)方法recoverA()和recoverB()抖甘,分別提供特定于該方法的恢復(fù)邏輯。由于每個(gè)方法都在@Retryable注解的value屬性中指定了不同的異常類型葫慎,因此Spring框架可以區(qū)分它們衔彻,并在適當(dāng)?shù)臅r(shí)候調(diào)用相應(yīng)的方法。

1.4.3 如果異常也一樣呢

如果多個(gè)方法在拋出相同的異常時(shí)都需要進(jìn)行重試和恢復(fù)操作偷办,你可以在每個(gè)方法上使用相同的@Retryable和@Recover注解艰额。在這種情況下,Spring框架會(huì)自動(dòng)根據(jù)需要調(diào)用相應(yīng)的恢復(fù)方法椒涯。

例如柄沮,以下示例演示了在同一個(gè)類中有多個(gè)方法拋出相同異常,并且都需要重試和恢復(fù)的情況:

@Service
public class MyService {
 
    @Retryable(RuntimeException.class)
    public void methodA() {
        // Some code that may throw a RuntimeException
    }
 
    @Retryable(RuntimeException.class)
    public void methodB() {
        // Some code that may throw a RuntimeException
    }
 
    @Recover
    public void recover() {
        // Recovery logic goes here
    }
}

在上面的示例中逐工,methodA()和methodB()方法都標(biāo)記為@Retryable注解,以便在拋出RuntimeException異常時(shí)進(jìn)行重試漂辐。由于它們都使用相同的異常類型和重試策略泪喊,因此它們也可以使用相同的恢復(fù)方法recover()。在最終重試失敗時(shí)髓涯,Spring框架將調(diào)用recover()方法以提供恢復(fù)邏輯袒啼。

1.4.4 那怎么在recover中知道是哪個(gè)方法發(fā)生了異常呢

@Recover注解標(biāo)記的恢復(fù)方法中,可以通過方法的參數(shù)獲取拋出異常的方法和異常信息纬纪。具體來說蚓再,@Recover方法可以接受與@Retryable注解標(biāo)記的方法相同的參數(shù),以便在恢復(fù)操作中訪問異常信息和方法參數(shù)包各。

例如摘仅,以下示例演示了在@Retryable方法和@Recover方法中訪問異常和方法參數(shù)的方式:

@Service
public class MyService {
 
    @Retryable(RuntimeException.class)
    public void methodA(String arg) {
        // Some code that may throw a RuntimeException
    }
 
    @Retryable(RuntimeException.class)
    public void methodB(int arg) {
        // Some code that may throw a RuntimeException
    }
 
    @Recover
    public void recover(Exception e, Object... args) {
        if (args[0] instanceof String) {
            // Recovery logic for methodA() goes here
        } else if (args[0] instanceof Integer) {
            // Recovery logic for methodB() goes here
        }
    }
}

在上面的示例中,methodA()和methodB()方法都標(biāo)記為@Retryable注解问畅,以便在拋出RuntimeException異常時(shí)進(jìn)行重試娃属。在@Recover注解標(biāo)記的恢復(fù)方法中,可以通過方法的參數(shù)訪問拋出異常的方法和方法參數(shù)护姆。在上述示例中矾端,我們檢查第一個(gè)參數(shù)的類型以確定是哪個(gè)方法拋出了異常,從而提供相應(yīng)的恢復(fù)邏輯卵皂。

需要注意的是秩铆,@Retryable方法和@Recover方法中的參數(shù)類型和數(shù)量必須相同,否則Spring框架將無法確定哪個(gè)方法拋出了異常灯变。

2 重試之Guava-Retry

2.1 簡(jiǎn)介

Guava retryer工具與spring-retry類似殴玛,都是通過定義重試者角色來包裝正常邏輯重試捅膘,但是Guava retryer有更優(yōu)的策略定義,在支持重試次數(shù)和重試頻度控制基礎(chǔ)上族阅,能夠兼容支持多個(gè)異陈耍或者自定義實(shí)體對(duì)象的重試源定義,讓重試功能有更多的靈活性坦刀。
Guava Retryer也是線程安全的愧沟,入口調(diào)用邏輯采用的是Java.util.concurrent.Callablecall方法,示例代碼如下:

2.2 pom.xml

pom.xml加入依賴

  <!-- https://mvnrepository.com/artifact/com.github.rholder/guava-retrying -->
    <dependency>
        <groupId>com.github.rholder</groupId>
        <artifactId>guava-retrying</artifactId>
        <version>2.0.0</version>
    </dependency>

2.3 代碼操作

更改一下測(cè)試的任務(wù)方法

package com.zgd.demo.thread.retry;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomUtils;
import org.springframework.remoting.RemoteAccessException;

/**
 * @Author: zgd
 * @Description:
 */
@Slf4j
public class RetryDemoTask {


  /**
   * 重試方法
   * @return
   */
  public static boolean retryTask(String param)  {
    log.info("收到請(qǐng)求參數(shù):{}",param);

    int i = RandomUtils.nextInt(0,11);
    log.info("隨機(jī)生成的數(shù):{}",i);
    if (i < 2) {
      log.info("為0,拋出參數(shù)異常.");
      throw new IllegalArgumentException("參數(shù)異常");
    }else if (i  < 5){
      log.info("為1,返回true.");
      return true;
    }else if (i < 7){
      log.info("為2,返回false.");
      return false;
    }else{
      //為其他
        log.info("大于2,拋出自定義異常.");
        throw new RemoteAccessException("大于2,拋出自定義異常");
      }
    }

}

這里設(shè)定跟Spring-Retry不一樣鲤遥,我們可以根據(jù)返回的結(jié)果來判斷是否重試沐寺,比如返回false我們就重試

package com.zgd.demo.thread.retry.guava;

import com.github.rholder.retry.*;
import com.zgd.demo.thread.retry.RetryDemoTask;
import org.junit.Test;
import org.springframework.remoting.RemoteAccessException;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;

/**
 * @Author: zgd
 * @Description:
 */
public class GuavaRetryTest {


  @Test
  public void fun01(){
    // RetryerBuilder 構(gòu)建重試實(shí)例 retryer,可以設(shè)置重試源且可以支持多個(gè)重試源,可以配置重試次數(shù)或重試超時(shí)時(shí)間盖奈,以及可以配置等待時(shí)間間隔
    Retryer<Boolean> retryer = RetryerBuilder.<Boolean> newBuilder()
            .retryIfExceptionOfType(RemoteAccessException.class)//設(shè)置異常重試源
            .retryIfResult(res-> res==false)  //設(shè)置根據(jù)結(jié)果重試
            .withWaitStrategy(WaitStrategies.fixedWait(3, TimeUnit.SECONDS)) //設(shè)置等待間隔時(shí)間
            .withStopStrategy(StopStrategies.stopAfterAttempt(3)) //設(shè)置最大重試次數(shù)
            .build();

    try {
      retryer.call(() -> RetryDemoTask.retryTask("abc"));
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

}

2.4 運(yùn)行結(jié)果

遇到了我們指定的需要重試的異常混坞,進(jìn)行重試,間隔是3秒


在這里插入圖片描述

重試次數(shù)超過了最大重試次數(shù)

在這里插入圖片描述

返回為true钢坦,直接結(jié)束重試


在這里插入圖片描述

遇到了沒有指定重試的異常究孕,結(jié)束重試


在這里插入圖片描述

返回false,重試

在這里插入圖片描述

2.5 Guava配置策略

我們可以更靈活的配置重試策略爹凹,比如:

  • retryIfExceptionretryIfException厨诸,拋出 runtime 異常、checked 異常時(shí)都會(huì)重試禾酱,但是拋出 error 不會(huì)重試微酬。
  • retryIfRuntimeExceptionretryIfRuntimeException 只會(huì)在拋 runtime 異常的時(shí)候才重試,checked 異常和error 都不重試颤陶。
  • retryIfExceptionOfTyperetryIfExceptionOfType允許我們只在發(fā)生特定異常的時(shí)候才重試颗管,比如NullPointerExceptionIllegalStateException 都屬于 runtime 異常,也包括自定義的error滓走。
    如:
    retryIfExceptionOfType(NullPointerException.class)垦江,只在拋出空指針異常重試
  • retryIfResult: retryIfResult 可以指定Callable 方法在返回值的時(shí)候進(jìn)行重試,如
    • 返回false重試
      .retryIfResult(Predicates.equalTo(false))
    • _error結(jié)尾才重試
      .retryIfResult(Predicates.containsPattern("_error$"))
    • 返回為空時(shí)重試
      .retryIfResult(res-> res==null)
  • RetryListener: 當(dāng)發(fā)生重試之后搅方,假如我們需要做一些額外的處理動(dòng)作疫粥,比如log一下異常,那么可以使用RetryListener腰懂。每次重試之后梗逮,guava-retrying 會(huì)自動(dòng)回調(diào)我們注冊(cè)的監(jiān)聽⌒辶铮可以注冊(cè)多個(gè)RetryListener慷彤,會(huì)按照注冊(cè)順序依次調(diào)用。
.withRetryListener(new RetryListener {      
 @Override    
   public <T> void onRetry(Attempt<T> attempt) {  
               logger.error("第【{}】次調(diào)用失敗" , attempt.getAttemptNumber());  
          } 
 }) 

2.6 主要接口

序號(hào) 接口 描述 備注
1 Attempt 一次執(zhí)行任務(wù)
2 AttemptTimeLimiter 單次任務(wù)執(zhí)行時(shí)間限制 如果單詞任務(wù)執(zhí)行超時(shí),則終止執(zhí)行當(dāng)前任務(wù)
3 BlockStrategies 任務(wù)阻塞策略 通俗的講就是當(dāng)前任務(wù)執(zhí)行完底哗,下次任務(wù)還沒開始這段時(shí)間做什么岁诉,默認(rèn)策略為:BlockStrategies.THREAD_SLEEP_STRATEGY
4 RetryException 重試異常
5 RetryListener 自定義重試監(jiān)聽器 可以用于異步記錄錯(cuò)誤日志
6 StopStrategy 停止重試策略
7 WaitStrategy 等待時(shí)長(zhǎng)策略 (控制時(shí)間間隔),返回結(jié)果為下次執(zhí)行時(shí)長(zhǎng)

StopStrategy跋选,停止重試策略涕癣,提供三種

  • StopAfterDelayStrategy
    設(shè)定一個(gè)最長(zhǎng)允許的執(zhí)行時(shí)間,比如設(shè)定最長(zhǎng)執(zhí)行10S前标,無論任務(wù)執(zhí)行次數(shù)坠韩,只要重試的時(shí)候超出了最長(zhǎng)時(shí)間最铁,則任務(wù)終止鞭达,并返回重試異常RetryException
  • NeverStopStrategy
    不停止倔叼,用于需要一直輪訓(xùn)知道返回期望結(jié)果的情況:
  • StopAfterAttemptStrategy
    設(shè)定最大重試次數(shù)厌漂,如果超出最大重試次數(shù)則停止重試,并返回重試異常:

WaitStrategy低散,等待時(shí)長(zhǎng)策略

  • FixedWaitStrategy
    固定等待時(shí)長(zhǎng)策略羽历,
  • RandomWaitStrategy
    隨機(jī)等待時(shí)長(zhǎng)策略 (可以提供一個(gè)最小和最大時(shí)長(zhǎng)座舍,等待時(shí)長(zhǎng)為其區(qū)間隨機(jī)值)
  • IncrementingWaitstrategy
    遞增等待時(shí)長(zhǎng)策略 (提供一個(gè)初始值和步長(zhǎng)稽犁,等待時(shí)間隨重試次數(shù)增加而增加)
  • ExponentialWaitstrategy
    指數(shù)等待時(shí)長(zhǎng)策略;
  • FibonacciWaitStrategy
    Fibonacci等待時(shí)長(zhǎng)策略焰望,
  • ExceptionWaitStrategy
    異常時(shí)長(zhǎng)等待策略;
  • CompositeWaitStrategy
    復(fù)合時(shí)長(zhǎng)等待策略;

3 使用斷路器Hystrix實(shí)現(xiàn)熔斷機(jī)制

除了重試機(jī)制外,熔斷機(jī)制也是一種常見的容錯(cuò)處理手段已亥。Hystrix是一款流行的斷路器實(shí)現(xiàn)庫(kù)熊赖,可以與Spring Boot集成,用于實(shí)現(xiàn)熔斷機(jī)制陷猫。

3.1 添加依賴

在pom.xml中添加Hystrix的依賴:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

3.2 配置啟用Hystrix

Spring Boot的主類上添加@EnableHystrix注解:

@SpringBootApplication
@EnableHystrix
public class YourApplication {
    public static void main(String[] args) {
        SpringApplication.run(YourApplication.class, args);
    }
}

3.3 使用Hystrix實(shí)現(xiàn)熔斷

3.3.1 代碼示例

import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;

@Service
public class ThirdPartyService {

    @HystrixCommand(fallbackMethod = "fallback")
    public String callThirdPartyApi() {
        // 調(diào)用第三方API的邏輯
        // ...
    }

    public String fallback() {
        // 熔斷時(shí)的降級(jí)邏輯
        // ...
    }
}

在上述示例中秫舌,通過@HystrixCommand注解標(biāo)記了callThirdPartyApi方法的妖,指定了熔斷時(shí)執(zhí)行的降級(jí)方法fallback

文章參考:
https://mp.weixin.qq.com/s/nUD5kT2BcEEbmfeyG7rnDQ
https://mp.weixin.qq.com/s/pEPHzbPdmCsNeFeF2LwlXg

4 Fast-Retry

4.1 前言

假設(shè)系統(tǒng)里有100萬個(gè)用戶绣檬,然后要輪詢重試的獲取每個(gè)用戶的身份信息, 如果還在使用SpringRetry和GuavaRetry 之類的這種單任務(wù)的同步重試框架,那可能到猴年馬月也處理不完嫂粟, 即使加再多的機(jī)器和線程也是杯水車薪娇未, 而Fast-Retry正是為這種場(chǎng)景而生

4.1.1 簡(jiǎn)介

fast-retry是一個(gè)高性能的多任務(wù)重試框架,支持百萬級(jí)任務(wù)的異步重試星虹、以及支持編程式和注解聲明式等多種使用方式零抬、 也支持自定義結(jié)果重試邏輯。
Github 項(xiàng)目地址:https://github.com/burukeYou/fast-retry
與主流的Spring-Retry, Guava-Retry等單任務(wù)同步重試框架不同宽涌,Fast-Retry是一個(gè)支持異步重試框架平夜,支持異步任務(wù)的重試、超時(shí)等待卸亮、回調(diào)忽妒。Spring-Retry, Guava-Retry均無法支持大批量任務(wù)的重試,即使加入線程池也無法解決,因?yàn)閷?shí)際每個(gè)重試任務(wù)都是單獨(dú)的同步邏輯段直,然后會(huì)會(huì)占用過多線程資源導(dǎo)致大量任務(wù)在等待處理吃溅,隨著任務(wù)數(shù)的增加,系統(tǒng)吞吐量大大降低鸯檬,性能指數(shù)級(jí)降低决侈,而Fast-Retry在異步重試下的性能是前者的指數(shù)倍。

即使拋開性能不談喧务, SpringRetry使用繁瑣赖歌,不支持根據(jù)結(jié)果的進(jìn)行重試,GuavaRetry雖然支持蹂楣,但是又沒有提供注解聲明式的使用俏站。

4.1.2 fast-retry相比其它重試框架快在哪里

與其說快在哪,不如說同步型重試框架慢在哪痊土。因?yàn)橥街卦囀?code>阻塞的同步的肄扎,比如有 100 個(gè)重試任務(wù),每個(gè)重試任務(wù)要重試 1 分鐘赁酝,線程池有 10 個(gè)線程池犯祠,最多只能同時(shí)處理10 個(gè)重試任務(wù)。
也就是說起碼要 10 分鐘后才去執(zhí)行剩下的 90 個(gè)任務(wù)酌呆。最后 100 個(gè)任務(wù)輪詢完都要 100 分鐘了衡载。實(shí)際沒必要等它輪詢完一個(gè)任務(wù)才去執(zhí)行下一個(gè)任務(wù),它開始輪詢一個(gè)任務(wù)后就可以開始去執(zhí)行下一個(gè)任務(wù)了隙袁。
具體來說痰娱,Fast-Retry主要體現(xiàn)在以下幾個(gè)方面:

  • 異步執(zhí)行:Fast-Retry 通常采用異步方式執(zhí)行重試邏輯,這意味著它可以在等待重試間隔時(shí)不阻塞主線程菩收,從而提高應(yīng)用程序的整體響應(yīng)性和吞吐量梨睁。
  • 非阻塞 I/O:在處理需要 I/O 操作(如網(wǎng)絡(luò)請(qǐng)求、文件讀寫等)的重試時(shí)娜饵,F(xiàn)ast-Retry 可以利用非阻塞 I/O 機(jī)制坡贺,這樣在等待 I/O 操作完成時(shí)不會(huì)占用寶貴的線程資源。
  • 優(yōu)化的重試策略:Fast-Retry 允許用戶自定義重試策略箱舞,包括重試次數(shù)遍坟、重試間隔、退避算法等晴股。通過智能的退避算法(如指數(shù)退避)愿伴,它可以在保持高效率的同時(shí)減少對(duì)資源的不必要消耗。
  • 資源利用:Fast-Retry 框架可能會(huì)優(yōu)化資源的使用电湘,例如通過復(fù)用連接或線程來減少創(chuàng)建和銷毀資源的開銷隔节。
  • 錯(cuò)誤處理:Fast-Retry 能夠快速識(shí)別和處理重試中的錯(cuò)誤万搔,減少錯(cuò)誤處理的時(shí)間開銷。
  • 集成和擴(kuò)展性:Fast-Retry 框架往往設(shè)計(jì)得易于集成和擴(kuò)展官帘,這意味著它可以快速地被添加到現(xiàn)有的系統(tǒng)中瞬雹,并且可以根據(jù)需要進(jìn)行定制。
  • 避免不必要的重試:Fast-Retry 能夠根據(jù)錯(cuò)誤類型或其他條件判斷是否需要重試刽虹,避免在明顯無望的情況下進(jìn)行無效的重試嘗試酗捌。
  • 性能監(jiān)控:Fast-Retry 可能包含性能監(jiān)控功能,這有助于及時(shí)發(fā)現(xiàn)性能瓶頸并進(jìn)行優(yōu)化涌哲。

注意胖缤,F(xiàn)ast-Retry 的具體實(shí)現(xiàn)可能會(huì)根據(jù)不同的編程語言和框架有所不同。

4.2 使用例子

4.2.1 pom.xml

<dependency>
    <groupId>io.github.burukeyou</groupId>
    <artifactId>fast-retry-all</artifactId>
    <version>0.2.0</version>
</dependency>

有以下三種方式去構(gòu)建我們的重試任務(wù)

4.2.2 使用重試隊(duì)列

RetryTask就是可以配置我們重試任務(wù)的一些邏輯阀圾,比如怎么重試哪廓,怎么獲取重試結(jié)果,隔多久后重試初烘,在什么情況下重試涡真。它可以幫助我們更加自由的去構(gòu)建重試任務(wù)的邏輯。但如果只是簡(jiǎn)單使用肾筐,強(qiáng)烈建議使用FastRetryBuilder 或者 @FastRetry注解
RetryQueue就是一個(gè)執(zhí)行和調(diào)度我們重試任務(wù)的核心角色哆料,其在使用上與線程池的API方法基本一致

ExecutorService executorService = Executors.newFixedThreadPool(8);
RetryQueue queue = new FastRetryQueue(executorService);
RetryTask<String> task = new RetryTask<String>() {
    int result = 0 ;

    // 下一次重試的間隔
    @Override
    public long waitRetryTime() {
        return 2000;
    }

    // 執(zhí)行重試,每次重試回調(diào)此方法
    @Override
    public boolean retry() {
        return ++result < 5;
    }

     // 獲取重試結(jié)果
    @Override
    public String getResult() {
        return  result + "";
    }
};
CompletableFuture<String> future = queue.submit(task);
log.info("任務(wù)結(jié)束 結(jié)果:{}",future.get());

4.2.3 使用FastRetryBuilder

底層還是使用的RetryQueue去處理吗铐, 只是幫我們簡(jiǎn)化了構(gòu)建RetryTask的邏輯

RetryResultPolicy<String> resultPolicy = result -> result.equals("444");
FastRetryer<String> retryer = FastRetryBuilder.<String>builder()
        .attemptMaxTimes(3)
        .waitRetryTime(3, TimeUnit.SECONDS)
        .retryIfException(true)
        .retryIfExceptionOfType(TimeoutException.class)
        .exceptionRecover(true)
        .resultPolicy(resultPolicy)
        .build();

CompletableFuture<String> future = retryer.submit(() -> {
    log.info("重試");
    //throw new Exception("test");
    //int i = 1/0;
    if (0 < 10){
        throw new TimeoutException("test");
    }
    return "444";
});

String o = future.get();
log.info("結(jié)果{}", o);

4.2.4 使用@FastRetry注解

底層還是使用的RetryQueue去處理东亦, 只是幫我們簡(jiǎn)化了構(gòu)建RetryTask的邏輯,并且與Spring進(jìn)行整合能對(duì)Springbean標(biāo)記了FastRetry注解的方法進(jìn)行代理唬渗, 提供了重試任務(wù)注解聲明式的使用方式

  • 依賴Spring環(huán)境典阵,所以需要在Spring配置類加上@EnableFastRetry注解啟用配置 , 這個(gè)@FastRetry注解的使用才會(huì)生效
  • 如果將結(jié)果類型使用CompletableFuture包裝镊逝,自動(dòng)進(jìn)行異步輪詢返回壮啊,否則同步阻塞等待重試結(jié)果(推薦)

下面定義等價(jià)于 RetryQueue.execute方法

// 如果發(fā)生異常,每隔兩秒重試一次
@FastRetry(retryWait = @RetryWait(delay = 2))
public String retryTask(){
    return "success";
}

下面定義等價(jià)于 RetryQueue.submit方法,支持異步輪詢

@FastRetry(retryWait = @RetryWait(delay = 2))
public CompletableFuture<String> retryTask(){
    return CompletableFuture.completedFuture("success");
}

4.2.5 自定義重試注解

如果不喜歡或者需要更加通用化的貼近業(yè)務(wù)的重試注解蹋半,提供一些默認(rèn)的參數(shù)和處理邏輯他巨,可以自行定義一個(gè)重試注解并標(biāo)記上@FastRetry并指定factory充坑,然后實(shí)現(xiàn)AnnotationRetryTaskFactory接口實(shí)現(xiàn)自己的構(gòu)建重試任務(wù)的邏輯即可减江。@FastRetry默認(rèn)實(shí)現(xiàn)就是:FastRetryAnnotationRetryTaskFactory

無論是使用以上哪種方式去構(gòu)建重試任務(wù),都建議使用異步重試的方法捻爷,即返回結(jié)果是CompletableFuture的方法辈灼, 然后使用CompletableFuturewhenComplete方法去等待異步重試任務(wù)的執(zhí)行結(jié)果。

4.3 實(shí)際操作

假如有一個(gè)天氣服務(wù)的重試任務(wù)也榄,需要重試N次才可能獲取到某城市的天氣情況巡莹。分別使用Fast-Retry注解和Spring-Retry注解去并發(fā)獲取1000個(gè)城市的天氣情況司志,看下系統(tǒng)耗時(shí)。同樣的邏輯降宅,Spring-Retry需要1256秒左右骂远,F(xiàn)ast-Retry只需要10秒左右

// 天氣服務(wù)
@Component
public class WeatherService {
    
    // Fast-Retry  重試獲取天氣城市天氣情況
    @FastRetry(
            maxAttempts = 100,
            retryWait = @RetryWait(delay = 2,timeUnit = TimeUnit.SECONDS))
    public CompletableFuture<WeatherResult> getFutureWeatherForCompare(String cityName){
        log.info("WeatherService進(jìn)行重試  次數(shù):{} 城市: {}",++index,cityName);
        WeatherResult weather = WeatherServer.getWeather(cityName);
        if (weather == null){
            //繼續(xù)重試
            throw new RuntimeException("模擬異常進(jìn)行重試");
        }

        return FastRetryBuilder.of(weather);
    }

   // Spring-Retry  重試獲取天氣城市天氣情況
    @Retryable(maxAttempts = 100,backoff = @Backoff(delay = 2000))
    public WeatherResult getSpringWeatherForCompare(String cityName){
        log.info("WeatherService進(jìn)行重試  次數(shù):{} 城市: {}",++index,cityName);
        WeatherResult weather = WeatherServer.getWeather(cityName);
        if (weather == null){
            //繼續(xù)重試
            throw new RuntimeException("模擬異常進(jìn)行重試");
        }
        return weather;
    }

}

使用Spring-Retry去執(zhí)行1000個(gè)重試任務(wù)

/**
 * spring-retry注解-測(cè)試
 * @throws Exception
 */
@Test
public void testFastRetryManyTaskForSpring() throws Exception {
    List<CompletableFuture<WeatherResult>> futures = new ArrayList<>();
    ExecutorService pool = Executors.newFixedThreadPool(8);

    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    int taskSize = 1000;
    for (int i = 0; i < taskSize; i++) {
        WeatherService taskWeatherService = context.getBean(WeatherService.class);
        CompletableFuture<WeatherResult> testFuture = new CompletableFuture<>();
        futures.add(testFuture);

        String cityName = "北京" + i;
        pool.execute(() -> {
            WeatherResult weather = taskWeatherService.getSpringWeatherForCompare(cityName);
            testFuture.complete(weather);
        });
    }

    CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();

    System.out.println("所有任務(wù)完成");
    for (CompletableFuture<WeatherResult> future : futures) {
        WeatherResult weatherResult = future.get();
        log.info("城市輪詢結(jié)束  result:{}",weatherResult.data);
    }

    stopWatch.stop();
    log.info("Spring-Retry測(cè)試總耗時(shí)  任務(wù)數(shù):{} 耗時(shí):{}",taskSize,stopWatch.getTotalTimeSeconds());
}

使用Fast-Retry去執(zhí)行1000個(gè)重試任務(wù)

/**
 * 測(cè)試FastRetry注解測(cè)試
 * @throws Exception
 */
@Test
public  void testFastRetryManyTask() throws Exception {

    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    int taskSize = 1000;

    List<CompletableFuture<WeatherResult>> futures = new ArrayList<>();
    for (int i = 0; i < taskSize; i++) {
        WeatherService taskWeatherService = context.getBean(WeatherService.class);
        String cityName = "北京" + i;
        CompletableFuture<WeatherResult> weather = taskWeatherService.getFutureWeatherForCompare(cityName);
        futures.add(weather);
    }
    CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
    System.out.println("所有任務(wù)完成");
    for (CompletableFuture<WeatherResult> future : futures) {
        WeatherResult weatherResult = future.get();
        log.info("城市輪詢結(jié)束  result:{}",weatherResult.data);
    }
    stopWatch.stop();
    log.info("FastRetry測(cè)試總耗時(shí)  任務(wù)數(shù):{} 耗時(shí):{}",taskSize,stopWatch.getTotalTimeSeconds());
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市腰根,隨后出現(xiàn)的幾起案子激才,更是在濱河造成了極大的恐慌,老刑警劉巖额嘿,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件瘸恼,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡册养,警方通過查閱死者的電腦和手機(jī)东帅,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來球拦,“玉大人靠闭,你說我怎么就攤上這事】擦叮” “怎么了阎毅?”我有些...
    開封第一講書人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)点弯。 經(jīng)常有香客問我扇调,道長(zhǎng),這世上最難降的妖魔是什么抢肛? 我笑而不...
    開封第一講書人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任狼钮,我火速辦了婚禮,結(jié)果婚禮上捡絮,老公的妹妹穿的比我還像新娘熬芜。我一直安慰自己,他們只是感情好福稳,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開白布涎拉。 她就那樣靜靜地躺著,像睡著了一般的圆。 火紅的嫁衣襯著肌膚如雪鼓拧。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,125評(píng)論 1 297
  • 那天越妈,我揣著相機(jī)與錄音季俩,去河邊找鬼。 笑死梅掠,一個(gè)胖子當(dāng)著我的面吹牛酌住,可吹牛的內(nèi)容都是我干的店归。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼酪我,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼消痛!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起都哭,我...
    開封第一講書人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤肄满,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后质涛,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體稠歉,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年汇陆,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了怒炸。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡毡代,死狀恐怖阅羹,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情教寂,我是刑警寧澤捏鱼,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站酪耕,受9級(jí)特大地震影響导梆,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜迂烁,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一看尼、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧盟步,春花似錦藏斩、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至黄橘,卻和暖如春兆览,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背旬陡。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來泰國(guó)打工拓颓, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留语婴,地道東北人描孟。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓驶睦,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親匿醒。 傳聞我的和親對(duì)象是個(gè)殘疾皇子场航,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

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