更好的 java 重試框架 sisyphus 背后的故事

sisyphus 綜合了 spring-retry 和 gauva-retrying 的優(yōu)勢既忆,使用起來也非常靈活倦始。

今天谐宙,讓我們一起看一下西西弗斯背后的故事。

情景導入

簡單的需求

產(chǎn)品經(jīng)理:實現(xiàn)一個按條件,查詢用戶信息的服務。

小明:好的须鼎。沒問題。

代碼

  • UserService.java
public interface UserService {

    /**
     * 根據(jù)條件查詢用戶信息
     * @param condition 條件
     * @return User 信息
     */
    User queryUser(QueryUserCondition condition);

}
  • UserServiceImpl.java
public class UserServiceImpl implements UserService {

    private OutService outService;

    public UserServiceImpl(OutService outService) {
        this.outService = outService;
    }

    @Override
    public User queryUser(QueryUserCondition condition) {
        outService.remoteCall();
        return new User();
    }

}

談話

項目經(jīng)理:這個服務有時候會失敗府蔗,你看下晋控。

小明:OutService 在是一個 RPC 的外部服務,但是有時候不穩(wěn)定姓赤。

項目經(jīng)理:如果調(diào)用失敗了赡译,你可以調(diào)用的時候重試幾次。你去看下重試相關的東西

重試

重試作用

對于重試是有場景限制的不铆,不是什么場景都適合重試蝌焚,比如參數(shù)校驗不合法、寫操作等(要考慮寫是否冪等)都不適合重試誓斥。

遠程調(diào)用超時只洒、網(wǎng)絡突然中斷可以重試。在微服務治理框架中劳坑,通常都有自己的重試與超時配置毕谴,比如dubbo可以設置retries=1,timeout=500調(diào)用失敗只重試1次距芬,超過500ms調(diào)用仍未返回則調(diào)用失敗涝开。

比如外部 RPC 調(diào)用,或者數(shù)據(jù)入庫等操作框仔,如果一次操作失敗舀武,可以進行多次重試,提高調(diào)用成功的可能性离斩。

V1.0 支持重試版本

思考

小明:我手頭還有其他任務银舱,這個也挺簡單的。5 分鐘時間搞定他跛梗。

實現(xiàn)

  • UserServiceRetryImpl.java
public class UserServiceRetryImpl implements UserService {

    @Override
    public User queryUser(QueryUserCondition condition) {
        int times = 0;
        OutService outService = new AlwaysFailOutServiceImpl();

        while (times < RetryConstant.MAX_TIMES) {
            try {
                outService.remoteCall();
                return new User();
            } catch (Exception e) {
                times++;

                if(times >= RetryConstant.MAX_TIMES) {
                    throw new RuntimeException(e);
                }
            }
        }

        return null;
    }

}

V1.1 代理模式版本

易于維護

項目經(jīng)理:你的代碼我看了寻馏,功能雖然實現(xiàn)了,但是盡量寫的易于維護一點茄袖。

小明:好的操软。(心想嘁锯,是說要寫點注釋什么的宪祥?)

代理模式

為其他對象提供一種代理以控制對這個對象的訪問聂薪。

在某些情況下,一個對象不適合或者不能直接引用另一個對象蝗羊,而代理對象可以在客戶端和目標對象之間起到中介作用藏澳。

其特征是代理與委托類有同樣的接口。

實現(xiàn)

小明想到以前看過的代理模式耀找,心想用這種方式翔悠,原來的代碼改動量較少,以后想改起來也方便些野芒。

  • UserServiceProxyImpl.java
public class UserServiceProxyImpl implements UserService {

    private UserService userService = new UserServiceImpl();

    @Override
    public User queryUser(QueryUserCondition condition) {
        int times = 0;

        while (times < RetryConstant.MAX_TIMES) {
            try {
                return userService.queryUser(condition);
            } catch (Exception e) {
                times++;

                if(times >= RetryConstant.MAX_TIMES) {
                    throw new RuntimeException(e);
                }
            }
        }
        return null;
    }

}

V1.2 動態(tài)代理模式

方便拓展

項目經(jīng)理:小明啊蓄愁,這里還有個方法也是同樣的問題。你也給加上重試吧狞悲。

小明:好的撮抓。

小明心想,我在寫一個代理摇锋,但是轉(zhuǎn)念冷靜了下來丹拯,如果還有個服務也要重試怎么辦呢?

  • RoleService.java
public interface RoleService {

    /**
     * 查詢
     * @param user 用戶信息
     * @return 是否擁有權限
     */
    boolean hasPrivilege(User user);

}

代碼實現(xiàn)

  • DynamicProxy.java
public class DynamicProxy implements InvocationHandler {

    private final Object subject;

    public DynamicProxy(Object subject) {
        this.subject = subject;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        int times = 0;

        while (times < RetryConstant.MAX_TIMES) {
            try {
                // 當代理對象調(diào)用真實對象的方法時荸恕,其會自動的跳轉(zhuǎn)到代理對象關聯(lián)的handler對象的invoke方法來進行調(diào)用
                return method.invoke(subject, args);
            } catch (Exception e) {
                times++;

                if (times >= RetryConstant.MAX_TIMES) {
                    throw new RuntimeException(e);
                }
            }
        }

        return null;
    }

    /**
     * 獲取動態(tài)代理
     *
     * @param realSubject 代理對象
     */
    public static Object getProxy(Object realSubject) {
        //    我們要代理哪個真實對象乖酬,就將該對象傳進去,最后是通過該真實對象來調(diào)用其方法的
        InvocationHandler handler = new DynamicProxy(realSubject);
        return Proxy.newProxyInstance(handler.getClass().getClassLoader(),
                realSubject.getClass().getInterfaces(), handler);
    }

}
  • 測試代碼
@Test
public void failUserServiceTest() {
        UserService realService = new UserServiceImpl();
        UserService proxyService = (UserService) DynamicProxy.getProxy(realService);

        User user = proxyService.queryUser(new QueryUserCondition());
        LOGGER.info("failUserServiceTest: " + user);
}


@Test
public void roleServiceTest() {
        RoleService realService = new RoleServiceImpl();
        RoleService proxyService = (RoleService) DynamicProxy.getProxy(realService);

        boolean hasPrivilege = proxyService.hasPrivilege(new User());
        LOGGER.info("roleServiceTest: " + hasPrivilege);
}

V1.3 動態(tài)代理模式增強

對話

項目經(jīng)理:小明融求,你動態(tài)代理的方式是挺會偷懶的咬像,可是我們有的類沒有接口。這個問題你要解決一下生宛。

小明:好的施掏。(誰?寫服務竟然不定義接口)

  • ResourceServiceImpl.java
public class ResourceServiceImpl {

    /**
     * 校驗資源信息
     * @param user 入?yún)?     * @return 是否校驗通過
     */
    public boolean checkResource(User user) {
        OutService outService = new AlwaysFailOutServiceImpl();
        outService.remoteCall();
        return true;
    }

}

字節(jié)碼技術

小明看了下網(wǎng)上的資料茅糜,解決的辦法還是有的七芭。

  • CGLIB

CGLIB 是一個功能強大、高性能和高質(zhì)量的代碼生成庫蔑赘,用于擴展JAVA類并在運行時實現(xiàn)接口狸驳。

  • javassist

javassist (Java編程助手)使Java字節(jié)碼操作變得簡單。

它是Java中編輯字節(jié)碼的類庫;它允許Java程序在運行時定義新類缩赛,并在JVM加載類文件時修改類文件耙箍。

與其他類似的字節(jié)碼編輯器不同,Javassist提供了兩個級別的API:源級和字節(jié)碼級酥馍。

如果用戶使用源代碼級API辩昆,他們可以編輯類文件,而不需要了解Java字節(jié)碼的規(guī)范旨袒。

整個API只使用Java語言的詞匯表進行設計汁针。您甚至可以以源文本的形式指定插入的字節(jié)碼;Javassist動態(tài)編譯它术辐。

另一方面,字節(jié)碼級API允許用戶直接編輯類文件作為其他編輯器施无。

  • ASM

ASM 是一個通用的Java字節(jié)碼操作和分析框架辉词。

它可以用來修改現(xiàn)有的類或動態(tài)地生成類,直接以二進制形式猾骡。

ASM提供了一些通用的字節(jié)碼轉(zhuǎn)換和分析算法瑞躺,可以從這些算法中構(gòu)建自定義復雜的轉(zhuǎn)換和代碼分析工具。

ASM提供與其他Java字節(jié)碼框架類似的功能兴想,但主要關注性能幢哨。

因為它的設計和實現(xiàn)都盡可能地小和快,所以非常適合在動態(tài)系統(tǒng)中使用(當然也可以以靜態(tài)的方式使用嫂便,例如在編譯器中)嘱么。

實現(xiàn)

小明看了下,就選擇使用 CGLIB顽悼。

  • CglibProxy.java
public class CglibProxy implements MethodInterceptor {

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        int times = 0;

        while (times < RetryConstant.MAX_TIMES) {
            try {
                //通過代理子類調(diào)用父類的方法
                return methodProxy.invokeSuper(o, objects);
            } catch (Exception e) {
                times++;

                if (times >= RetryConstant.MAX_TIMES) {
                    throw new RuntimeException(e);
                }
            }
        }

        return null;
    }

    /**
     * 獲取代理類
     * @param clazz 類信息
     * @return 代理類結(jié)果
     */
    public Object getProxy(Class clazz){
        Enhancer enhancer = new Enhancer();
        //目標對象類
        enhancer.setSuperclass(clazz);
        enhancer.setCallback(this);
        //通過字節(jié)碼技術創(chuàng)建目標對象類的子類實例作為代理
        return enhancer.create();
    }

}
  • 測試
@Test
public void failUserServiceTest() {
   UserService proxyService = (UserService) new CglibProxy().getProxy(UserServiceImpl.class);

   User user = proxyService.queryUser(new QueryUserCondition());
   LOGGER.info("failUserServiceTest: " + user);
}

@Test
public void resourceServiceTest() {
   ResourceServiceImpl proxyService = (ResourceServiceImpl) new CglibProxy().getProxy(ResourceServiceImpl.class);
   boolean result = proxyService.checkResource(new User());
   LOGGER.info("resourceServiceTest: " + result);
}

V2.0 AOP 實現(xiàn)

對話

項目經(jīng)理:小明啊曼振,最近我在想一個問題。不同的服務蔚龙,重試的時候次數(shù)應該是不同的冰评。因為服務對穩(wěn)定性的要求各不相同啊。

小明:好的木羹。(心想甲雅,重試都搞了一周了,今天都周五了坑填。)

下班之前抛人,小明一直在想這個問題。剛好周末脐瑰,花點時間寫個重試小工具吧妖枚。

設計思路

  • 技術支持

spring

java 注解

  • 注解定義

注解可在方法上使用,定義需要重試的次數(shù)

  • 注解解析

攔截指定需要重試的方法苍在,解析對應的重試次數(shù)绝页,然后進行對應次數(shù)的重試。

實現(xiàn)

  • Retryable.java
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Retryable {

    /**
     * Exception type that are retryable.
     * @return exception type to retry
     */
    Class<? extends Throwable> value() default RuntimeException.class;

    /**
     * 包含第一次失敗
     * @return the maximum number of attempts (including the first failure), defaults to 3
     */
    int maxAttempts() default 3;

}
  • RetryAspect.java
@Aspect
@Component
public class RetryAspect {

    @Pointcut("execution(public * com.github.houbb.retry.aop..*.*(..)) &&" +
                      "@annotation(com.github.houbb.retry.aop.annotation.Retryable)")
    public void myPointcut() {
    }

    @Around("myPointcut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        Method method = getCurrentMethod(point);
        Retryable retryable = method.getAnnotation(Retryable.class);

        //1. 最大次數(shù)判斷
        int maxAttempts = retryable.maxAttempts();
        if (maxAttempts <= 1) {
            return point.proceed();
        }

        //2. 異常處理
        int times = 0;
        final Class<? extends Throwable> exceptionClass = retryable.value();
        while (times < maxAttempts) {
            try {
                return point.proceed();
            } catch (Throwable e) {
                times++;

                // 超過最大重試次數(shù) or 不屬于當前處理異常
                if (times >= maxAttempts ||
                        !e.getClass().isAssignableFrom(exceptionClass)) {
                    throw new Throwable(e);
                }
            }
        }

        return null;
    }

    private Method getCurrentMethod(ProceedingJoinPoint point) {
        try {
            Signature sig = point.getSignature();
            MethodSignature msig = (MethodSignature) sig;
            Object target = point.getTarget();
            return target.getClass().getMethod(msig.getName(), msig.getParameterTypes());
        } catch (NoSuchMethodException e) {
            throw new RuntimeException(e);
        }
    }

}

方法的使用

  • fiveTimes()

當前方法一共重試 5 次寂恬。
重試條件:服務拋出 AopRuntimeExption

@Override
@Retryable(maxAttempts = 5, value = AopRuntimeExption.class)
public void fiveTimes() {
    LOGGER.info("fiveTimes called!");
    throw new AopRuntimeExption();
}
  • 測試日志
2018-08-08 15:49:33.814  INFO  [main] com.github.houbb.retry.aop.service.impl.UserServiceImpl:66 - fiveTimes called!
2018-08-08 15:49:33.815  INFO  [main] com.github.houbb.retry.aop.service.impl.UserServiceImpl:66 - fiveTimes called!
2018-08-08 15:49:33.815  INFO  [main] com.github.houbb.retry.aop.service.impl.UserServiceImpl:66 - fiveTimes called!
2018-08-08 15:49:33.815  INFO  [main] com.github.houbb.retry.aop.service.impl.UserServiceImpl:66 - fiveTimes called!
2018-08-08 15:49:33.815  INFO  [main] com.github.houbb.retry.aop.service.impl.UserServiceImpl:66 - fiveTimes called!

java.lang.reflect.UndeclaredThrowableException
...

V3.0 spring-retry 版本

對話

周一來到公司续誉,項目經(jīng)理又和小明談了起來。

項目經(jīng)理:重試次數(shù)是滿足了初肉,但是重試其實應該講究策略酷鸦。比如調(diào)用外部,第一次失敗,可以等待 5S 在次調(diào)用臼隔,如果又失敗了嘹裂,可以等待 10S 再調(diào)用。躬翁。焦蘑。

小明:了解盯拱。

思考

可是今天周一盒发,還有其他很多事情要做。

小明在想狡逢,沒時間寫這個呀宁舰。看看網(wǎng)上有沒有現(xiàn)成的奢浑。

spring-retry

Spring Retry 為 Spring 應用程序提供了聲明性重試支持蛮艰。 它用于Spring批處理、Spring集成雀彼、Apache Hadoop(等等)的Spring壤蚜。

在分布式系統(tǒng)中,為了保證數(shù)據(jù)分布式事務的強一致性徊哑,大家在調(diào)用RPC接口或者發(fā)送MQ時袜刷,針對可能會出現(xiàn)網(wǎng)絡抖動請求超時情況采取一下重試操作。 大家用的最多的重試方式就是MQ了莺丑,但是如果你的項目中沒有引入MQ著蟹,那就不方便了。

還有一種方式梢莽,是開發(fā)者自己編寫重試機制萧豆,但是大多不夠優(yōu)雅。

注解式使用

  • RemoteService.java

重試條件:遇到 RuntimeException

重試次數(shù):3

重試策略:重試的時候等待 5S, 后面時間依次變?yōu)樵瓉淼?2 倍數(shù)昏名。

熔斷機制:全部重試失敗涮雷,則調(diào)用 recover() 方法。

@Service
public class RemoteService {

    private static final Logger LOGGER = LoggerFactory.getLogger(RemoteService.class);

    /**
     * 調(diào)用方法
     */
    @Retryable(value = RuntimeException.class,
               maxAttempts = 3,
               backoff = @Backoff(delay = 5000L, multiplier = 2))
    public void call() {
        LOGGER.info("Call something...");
        throw new RuntimeException("RPC調(diào)用異常");
    }

    /**
     * recover 機制
     * @param e 異常
     */
    @Recover
    public void recover(RuntimeException e) {
        LOGGER.info("Start do recover things....");
        LOGGER.warn("We meet ex: ", e);
    }

}
  • 測試
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
public class RemoteServiceTest {

    @Autowired
    private RemoteService remoteService;

    @Test
    public void test() {
        remoteService.call();
    }

}
  • 日志
2018-08-08 16:03:26.409  INFO 1433 --- [           main] c.g.h.r.spring.service.RemoteService     : Call something...
2018-08-08 16:03:31.414  INFO 1433 --- [           main] c.g.h.r.spring.service.RemoteService     : Call something...
2018-08-08 16:03:41.416  INFO 1433 --- [           main] c.g.h.r.spring.service.RemoteService     : Call something...
2018-08-08 16:03:41.418  INFO 1433 --- [           main] c.g.h.r.spring.service.RemoteService     : Start do recover things....
2018-08-08 16:03:41.425  WARN 1433 --- [           main] c.g.h.r.spring.service.RemoteService     : We meet ex: 

java.lang.RuntimeException: RPC調(diào)用異常
    at com.github.houbb.retry.spring.service.RemoteService.call(RemoteService.java:38) ~[classes/:na]
...

三次調(diào)用的時間點:

2018-08-08 16:03:26.409 
2018-08-08 16:03:31.414
2018-08-08 16:03:41.416

缺陷

spring-retry 工具雖能優(yōu)雅實現(xiàn)重試轻局,但是存在兩個不友好設計:

一個是重試實體限定為 Throwable 子類份殿,說明重試針對的是可捕捉的功能異常為設計前提的,但是我們希望依賴某個數(shù)據(jù)對象實體作為重試實體嗽交,
但 sping-retry框架必須強制轉(zhuǎn)換為Throwable子類卿嘲。

另一個就是重試根源的斷言對象使用的是 doWithRetry 的 Exception 異常實例,不符合正常內(nèi)部斷言的返回設計夫壁。

Spring Retry 提倡以注解的方式對方法進行重試拾枣,重試邏輯是同步執(zhí)行的,重試的“失敗”針對的是Throwable,
如果你要以返回值的某個狀態(tài)來判定是否需要重試梅肤,可能只能通過自己判斷返回值然后顯式拋出異常了司蔬。

@Recover 注解在使用時無法指定方法,如果一個類中多個重試方法姨蝴,就會很麻煩俊啼。

guava-retrying

談話

小華:我們系統(tǒng)也要用到重試

項目經(jīng)理:小明前段時間用了 spring-retry,分享下應該還不錯

小明:spring-retry 基本功能都有左医,但是必須是基于異常來進行控制授帕。如果你要以返回值的某個狀態(tài)來判定是否需要重試,可能只能通過自己判斷返回值然后顯式拋出異常了浮梢。

小華:我們項目中想根據(jù)對象的屬性來進行重試跛十。你可以看下 guava-retry,我很久以前用過秕硝,感覺還不錯芥映。

小明:好的。

guava-retrying

guava-retrying 模塊提供了一種通用方法远豺, 可以使用Guava謂詞匹配增強的特定停止奈偏、重試和異常處理功能來重試任意Java代碼。

  • 優(yōu)勢

guava retryer工具與spring-retry類似躯护,都是通過定義重試者角色來包裝正常邏輯重試惊来,但是Guava retryer有更優(yōu)的策略定義,在支持重試次數(shù)和重試頻度控制基礎上榛做,能夠兼容支持多個異逞湔担或者自定義實體對象的重試源定義,讓重試功能有更多的靈活性检眯。

Guava Retryer也是線程安全的厘擂,入口調(diào)用邏輯采用的是 java.util.concurrent.Callablecall() 方法

代碼例子

入門案例

遇到異常之后,重試 3 次停止

  • HelloDemo.java
public static void main(String[] args) {
    Callable<Boolean> callable = new Callable<Boolean>() {
        @Override
        public Boolean call() throws Exception {
            // do something useful here
            LOGGER.info("call...");
            throw new RuntimeException();
        }
    };

    Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder()
            .retryIfResult(Predicates.isNull())
            .retryIfExceptionOfType(IOException.class)
            .retryIfRuntimeException()
            .withStopStrategy(StopStrategies.stopAfterAttempt(3))
            .build();
    try {
        retryer.call(callable);
    } catch (RetryException | ExecutionException e) {
        e.printStackTrace();
    }

}
  • 日志
2018-08-08 17:21:12.442  INFO  [main] com.github.houbb.retry.guava.HelloDemo:41 - call...
com.github.rholder.retry.RetryException: Retrying failed to complete successfully after 3 attempts.
2018-08-08 17:21:12.443  INFO  [main] com.github.houbb.retry.guava.HelloDemo:41 - call...
2018-08-08 17:21:12.444  INFO  [main] com.github.houbb.retry.guava.HelloDemo:41 - call...
    at com.github.rholder.retry.Retryer.call(Retryer.java:174)
    at com.github.houbb.retry.guava.HelloDemo.main(HelloDemo.java:53)
Caused by: java.lang.RuntimeException
    at com.github.houbb.retry.guava.HelloDemo$1.call(HelloDemo.java:42)
    at com.github.houbb.retry.guava.HelloDemo$1.call(HelloDemo.java:37)
    at com.github.rholder.retry.AttemptTimeLimiters$NoAttemptTimeLimit.call(AttemptTimeLimiters.java:78)
    at com.github.rholder.retry.Retryer.call(Retryer.java:160)
    ... 1 more

總結(jié)

優(yōu)雅重試共性和原理

正常和重試優(yōu)雅解耦锰瘸,重試斷言條件實例或邏輯異常實例是兩者溝通的媒介刽严。

約定重試間隔,差異性重試策略避凝,設置重試超時時間舞萄,進一步保證重試有效性以及重試流程穩(wěn)定性。

都使用了命令設計模式管削,通過委托重試對象完成相應的邏輯操作倒脓,同時內(nèi)部封裝實現(xiàn)重試邏輯。

spring-retry 和 guava-retry 工具都是線程安全的重試含思,能夠支持并發(fā)業(yè)務場景的重試邏輯正確性崎弃。

優(yōu)雅重試適用場景

功能邏輯中存在不穩(wěn)定依賴場景甘晤,需要使用重試獲取預期結(jié)果或者嘗試重新執(zhí)行邏輯不立即結(jié)束。比如遠程接口訪問饲做,數(shù)據(jù)加載訪問线婚,數(shù)據(jù)上傳校驗等等。

對于異常場景存在需要重試場景盆均,同時希望把正常邏輯和重試邏輯解耦塞弊。

對于需要基于數(shù)據(jù)媒介交互,希望通過重試輪詢檢測執(zhí)行邏輯場景也可以考慮重試方案泪姨。

談話

項目經(jīng)理:我覺得 guava-retry 挺好的游沿,就是不夠方便。小明啊驴娃,你給封裝個基于注解的吧奏候。

小明:……

更好的實現(xiàn)

于是小明含淚寫下了 sisyphus.

java 重試框架——sisyphus

希望本文對你有所幫助循集,如果喜歡唇敞,歡迎點贊收藏轉(zhuǎn)發(fā)一波。

我是老馬咒彤,期待與你的下次重逢疆柔。

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市镶柱,隨后出現(xiàn)的幾起案子旷档,更是在濱河造成了極大的恐慌,老刑警劉巖歇拆,帶你破解...
    沈念sama閱讀 210,978評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鞋屈,死亡現(xiàn)場離奇詭異,居然都是意外死亡故觅,警方通過查閱死者的電腦和手機厂庇,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評論 2 384
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來输吏,“玉大人权旷,你說我怎么就攤上這事」峤Γ” “怎么了拄氯?”我有些...
    開封第一講書人閱讀 156,623評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長它浅。 經(jīng)常有香客問我译柏,道長,這世上最難降的妖魔是什么姐霍? 我笑而不...
    開封第一講書人閱讀 56,324評論 1 282
  • 正文 為了忘掉前任鄙麦,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘黔衡。我一直安慰自己蚓聘,他們只是感情好,可當我...
    茶點故事閱讀 65,390評論 5 384
  • 文/花漫 我一把揭開白布盟劫。 她就那樣靜靜地躺著夜牡,像睡著了一般。 火紅的嫁衣襯著肌膚如雪侣签。 梳的紋絲不亂的頭發(fā)上塘装,一...
    開封第一講書人閱讀 49,741評論 1 289
  • 那天,我揣著相機與錄音影所,去河邊找鬼蹦肴。 笑死,一個胖子當著我的面吹牛猴娩,可吹牛的內(nèi)容都是我干的阴幌。 我是一名探鬼主播,決...
    沈念sama閱讀 38,892評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼卷中,長吁一口氣:“原來是場噩夢啊……” “哼矛双!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起蟆豫,我...
    開封第一講書人閱讀 37,655評論 0 266
  • 序言:老撾萬榮一對情侶失蹤议忽,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后十减,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體栈幸,經(jīng)...
    沈念sama閱讀 44,104評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年帮辟,在試婚紗的時候發(fā)現(xiàn)自己被綠了速址。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,569評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡织阅,死狀恐怖壳繁,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情荔棉,我是刑警寧澤闹炉,帶...
    沈念sama閱讀 34,254評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站润樱,受9級特大地震影響渣触,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜壹若,卻給世界環(huán)境...
    茶點故事閱讀 39,834評論 3 312
  • 文/蒙蒙 一嗅钻、第九天 我趴在偏房一處隱蔽的房頂上張望皂冰。 院中可真熱鬧,春花似錦养篓、人聲如沸秃流。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,725評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽舶胀。三九已至,卻和暖如春碧注,著一層夾襖步出監(jiān)牢的瞬間嚣伐,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,950評論 1 264
  • 我被黑心中介騙來泰國打工萍丐, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留轩端,地道東北人。 一個月前我還...
    沈念sama閱讀 46,260評論 2 360
  • 正文 我出身青樓逝变,卻偏偏與公主長得像基茵,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子骨田,可洞房花燭夜當晚...
    茶點故事閱讀 43,446評論 2 348

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

  • 應用中需要實現(xiàn)一個功能耿导,需要將數(shù)據(jù)上傳到遠程存儲服務声怔,同時在返回成功情況下做其他操作态贤。這個功能不復雜,分為兩個步驟...
    逗逼程序員閱讀 2,846評論 0 1
  • 目錄 重試的使用場景 如何優(yōu)雅地設計重試實現(xiàn) guava-retrying基礎用法 guava-retrying實...
    西召閱讀 13,330評論 3 24
  • 原文: Ribbon——超時與重試date: 2019-04-26 18:57:04 [TOC] 前言 在上篇源碼...
    i蝸居年華_謝謝謝閱讀 3,809評論 0 3
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理醋火,服務發(fā)現(xiàn)悠汽,斷路器,智...
    卡卡羅2017閱讀 134,628評論 18 139
  • 子曰:小勝靠智芥驳,大勝靠德柿冲,常勝靠身體。 resilience4j是什么兆旬? Resilience4j 是受Netfl...
    大哥你先走閱讀 7,167評論 0 55