警惕看不见的重试机制:为什么使用RPC必须考虑幂等性

在RPC场景中因为重试或者没有实现幂等性而导致的重复数据问题,必须引起大家重视,有可能会造成例如一次购买创建多笔订单,一条通知信息被发送多次等问题,这是技术人员必须面对和解决的问题。

有人可能会说:当调用失败时程序并没有显示重试,为什么还会产生重复数据问题呢?这是因为即使没有显示重试,RPC框架在集群容错机制中自动进行了重试,这个问题必须引起关注。

本文我们以DUBBO框架为例分析为什么重试,怎么做重试,怎么做幂等三个问题。


1 为什么重试

如果简单对一个RPC交互过程进行分类,可以分为三类:响应成功、响应失败、没有响应。


对于响应成功和响应失败两种情况,消费者很好处理。因为响应信息明确,所以只要根据响应信息,继续处理成功或者失败逻辑即可。但是没有响应这种场景比较难处理,这是因为没有响应可能包含以下情况:

(1) 生产者根本没有接收到请求

(2) 生产者接收到请求并且已处理成功,但是消费者没有接收到响应

(3) 生产者接收到请求并且已处理失败,但是消费者没有接收到响应

假设你是一名RPC框架设计者,究竟是选择重试还是放弃调用呢?其实最终如何选择取决于业务特性,有的业务本身就具有幂等性,但是有的业务不能允许重试否则会造成重复数据。

那么谁对业务特性最熟悉呢?答案是消费者,因为消费者作为调用方肯定最熟悉自身业务,所以RPC框架只要提供一些策略供消费者选择即可。


2,怎么做重试?

2.1 集群容错策略

DUBBO作为一款优秀RPC框架,提供了如下集群容错策略供消费者选择:

Failover: 故障转移

Failfast: 快速失败

Failsafe: 安全失败

Failback: 异步重试

Forking:  并行调用

Broadcast:广播调用

(1) Failover

故障转移策略。作为默认策略,当消费发生异常时,通过负载均衡策略再选择一个生产者节点进行调用,直到达到重试次数

(2) Failfast

快速失败策略。消费者只消费一次服务,当发生异常时则直接抛出

(3) Failsafe

安全失败策略。消费者只消费一次服务,如果消费失败则包装一个空结果,不抛出异常

(4) Failback

异步重试策略。消费发生异常时返回一个空结果,失败请求将会进行异步重试。如果重试超过最大重试次数还不成功,放弃重试并不抛出异常

(5) Forking

并行调用策略。消费者通过线程池并发调用多个生产者,只要有一个成功就算成功

(6) Broadcast

广播调用策略。消费者遍历调用所有生产者节点,任何一个出现异常则抛出异常

2.2  源码分析

2.2.1 Failover

Failover故障转移策略作为默认策略,当消费发生异常时,通过负载均衡策略再选择一个生产者节点进行调用,直到达到重试次数。即使业务代码没有显示重试,也有可能多次执行消费逻辑从而造成重复数据:

public classFailoverClusterInvokerextendsAbstractClusterInvoker{

    publicFailoverClusterInvoker(Directory<T> directory){

        super(directory);

    }

    @Override

    publicResultdoInvoke(Invocation invocation,finalList> invokers, LoadBalance loadbalance)throwsRpcException{

        // 所有生产者Invokers

        List<Invoker<T>> copyInvokers = invokers;

        checkInvokers(copyInvokers, invocation);

        String methodName = RpcUtils.getMethodName(invocation);

        // 获取重试次数

        int len = getUrl().getMethodParameter(methodName, Constants.RETRIES_KEY, Constants.DEFAULT_RETRIES) + 1;

        if (len <= 0) {

            len = 1;

        }

        RpcException le = null;

        // 已经调用过的生产者

        List<Invoker<T>> invoked = new ArrayList<Invoker<T>>(copyInvokers.size());

        Set<String> providers = new HashSet<String>(len);

        // 重试直到达到最大次数

        for (int i = 0; i < len; i++) {

            if (i > 0) {

                // 如果当前实例被销毁则抛出异常

                checkWhetherDestroyed();

                // 根据路由策略选出可用生产者Invokers

                copyInvokers = list(invocation);

                // 重新检查

                checkInvokers(copyInvokers, invocation);

            }

            // 负载均衡选择一个生产者Invoker

            Invoker<T> invoker = select(loadbalance, invocation, copyInvokers, invoked);

            invoked.add(invoker);

            RpcContext.getContext().setInvokers((List) invoked);

            try {

                // 服务消费发起远程调用

                Result result = invoker.invoke(invocation);

                if (le != null && logger.isWarnEnabled()) {

                    logger.warn("Although retry the method " + methodName + " in the service " + getInterface().getName() + " was successful by the provider " + invoker.getUrl().getAddress() + ", but there have been failed providers " + providers + " (" + providers.size() + "/" + copyInvokers.size() + ") from the registry " + directory.getUrl().getAddress() + " on the consumer " + NetUtils.getLocalHost() + " using the dubbo version " + Version.getVersion() + ". Last error is: " + le.getMessage(), le);

                }

                // 有结果则返回

                return result;

            } catch (RpcException e) {

                // 业务异常直接抛出

                if (e.isBiz()) {

                    throw e;

                }

                le = e;

            } catch (Throwable e) {

                // RpcException不抛出继续重试

                le = new RpcException(e.getMessage(), e);

            } finally {

                // 保存已经访问过的生产者

                providers.add(invoker.getUrl().getAddress());

            }

        }

        throw new RpcException(le.getCode(), "Failed to invoke the method " + methodName + " in the service " + getInterface().getName() + ". Tried " + len + " times of the providers " + providers + " (" + providers.size() + "/" + copyInvokers.size() + ") from the registry " + directory.getUrl().getAddress() + " on the consumer " + NetUtils.getLocalHost() + " using the dubbo version " + Version.getVersion() + ". Last error is: " + le.getMessage(), le.getCause() != null ? le.getCause() : le);

    }

}

消费者调用生产者节点A发生RpcException异常时(例如超时异常),在未达到最大重试次数之前,消费者会通过负载均衡策略再次选择其它生产者节点消费。试想如果生产者节点A其实已经处理成功了,但是没有及时将成功结果返回给消费者,那么再次重试就可能造成重复数据问题。

2.2.2 Failfast

快速失败策略。消费者只消费一次服务,当发生异常时则直接抛出,不会进行重试:

public classFailfastClusterInvokerextendsAbstractClusterInvoker{

    publicFailfastClusterInvoker(Directory<T> directory){

        super(directory);

    }

    @Override

    publicResultdoInvoke(Invocation invocation, List<Invoker<T>> invokers, LoadBalance loadbalance)throwsRpcException{

        // 检查生产者Invokers是否合法

        checkInvokers(invokers, invocation);

        // 负载均衡选择一个生产者Invoker

        Invoker<T> invoker = select(loadbalance, invocation, invokers, null);

        try {

            // 服务消费发起远程调用

            return invoker.invoke(invocation);

        } catch (Throwable e) {

            // 服务消费失败不重试直接抛出异常

            if (e instanceof RpcException && ((RpcException) e).isBiz()) {

                throw (RpcException) e;

            }

            throw new RpcException(e instanceof RpcException ? ((RpcException) e).getCode() : 0,

                                   "Failfast invoke providers " + invoker.getUrl() + " " + loadbalance.getClass().getSimpleName()

                                   + " select from all providers " + invokers + " for service " + getInterface().getName()

                                   + " method " + invocation.getMethodName() + " on consumer " + NetUtils.getLocalHost()

                                   + " use dubbo version " + Version.getVersion()

                                   + ", but no luck to perform the invocation. Last error is: " + e.getMessage(),

                                   e.getCause() != null ? e.getCause() : e);

        }

    }

}

2.2.3 Failsafe

安全失败策略。消费者只消费一次服务,如果消费失败则包装一个空结果,不抛出异常,不会进行重试:

public classFailsafeClusterInvokerextendsAbstractClusterInvoker{

    private static final Logger logger = LoggerFactory.getLogger(FailsafeClusterInvoker.class);

    publicFailsafeClusterInvoker(Directory<T> directory){

        super(directory);

    }

    @Override

    publicResultdoInvoke(Invocation invocation, List<Invoker<T>> invokers, LoadBalance loadbalance)throwsRpcException{

        try {

            // 检查生产者Invokers是否合法

            checkInvokers(invokers, invocation);

            // 负载均衡选择一个生产者Invoker

            Invoker<T> invoker = select(loadbalance, invocation, invokers, null);

            // 服务消费发起远程调用

            return invoker.invoke(invocation);

        } catch (Throwable e) {

            // 消费失败包装为一个空结果对象

            logger.error("Failsafe ignore exception: " + e.getMessage(), e);

            return new RpcResult();

        }

    }

}

2.2.4 Failback

异步重试策略。消费发生异常时返回一个空结果,失败请求会进行异步重试。如果重试超过最大重试次数还不成功,放弃重试并不抛出异常:

public classFailbackClusterInvokerextendsAbstractClusterInvoker{

    private static final Logger logger = LoggerFactory.getLogger(FailbackClusterInvoker.class);

    private static final long RETRY_FAILED_PERIOD = 5;

    private final int retries;

    private final int failbackTasks;

    private volatile Timer failTimer;

    publicFailbackClusterInvoker(Directory<T> directory){

        super(directory);

        int retriesConfig = getUrl().getParameter(Constants.RETRIES_KEY, Constants.DEFAULT_FAILBACK_TIMES);

        if (retriesConfig <= 0) {

            retriesConfig = Constants.DEFAULT_FAILBACK_TIMES;

        }

        int failbackTasksConfig = getUrl().getParameter(Constants.FAIL_BACK_TASKS_KEY, Constants.DEFAULT_FAILBACK_TASKS);

        if (failbackTasksConfig <= 0) {

            failbackTasksConfig = Constants.DEFAULT_FAILBACK_TASKS;

        }

        retries = retriesConfig;

        failbackTasks = failbackTasksConfig;

    }

    privatevoidaddFailed(LoadBalance loadbalance, Invocation invocation, List<Invoker<T>> invokers, Invoker<T> lastInvoker){

        if (failTimer == null) {

            synchronized (this) {

                if (failTimer == null) {

                    // 创建定时器

                    failTimer = new HashedWheelTimer(new NamedThreadFactory("failback-cluster-timer", true), 1, TimeUnit.SECONDS, 32, failbackTasks);

                }

            }

        }

        // 构造定时任务

        RetryTimerTask retryTimerTask = new RetryTimerTask(loadbalance, invocation, invokers, lastInvoker, retries, RETRY_FAILED_PERIOD);

        try {

            // 定时任务放入定时器等待执行

            failTimer.newTimeout(retryTimerTask, RETRY_FAILED_PERIOD, TimeUnit.SECONDS);

        } catch (Throwable e) {

            logger.error("Failback background works error,invocation->" + invocation + ", exception: " + e.getMessage());

        }

    }

    @Override

    protectedResultdoInvoke(Invocation invocation, List<Invoker<T>> invokers, LoadBalance loadbalance)throwsRpcException{

        Invoker<T> invoker = null;

        try {

            // 检查生产者Invokers是否合法

            checkInvokers(invokers, invocation);

            // 负责均衡选择一个生产者Invoker

            invoker = select(loadbalance, invocation, invokers, null);

            // 消费服务发起远程调用

            return invoker.invoke(invocation);

        } catch (Throwable e) {

            logger.error("Failback to invoke method " + invocation.getMethodName() + ", wait for retry in background. Ignored exception: " + e.getMessage() + ", ", e);

            // 如果服务消费失败则记录失败请求

            addFailed(loadbalance, invocation, invokers, invoker);

            // 返回空结果

            return new RpcResult();

        }

    }

    @Override

    publicvoiddestroy(){

        super.destroy();

        if (failTimer != null) {

            failTimer.stop();

        }

    }

    /**

* RetryTimerTask

*/

    private classRetryTimerTaskimplementsTimerTask{

        private final Invocation invocation;

        private final LoadBalance loadbalance;

        private final List<Invoker<T>> invokers;

        private final int retries;

        private final long tick;

        private Invoker<T> lastInvoker;

        private int retryTimes = 0;

        RetryTimerTask(LoadBalance loadbalance, Invocation invocation, List<Invoker<T>> invokers, Invoker<T> lastInvoker, int retries, long tick) {

            this.loadbalance = loadbalance;

            this.invocation = invocation;

            this.invokers = invokers;

            this.retries = retries;

            this.tick = tick;

            this.lastInvoker = lastInvoker;

        }

        @Override

        publicvoidrun(Timeout timeout){

            try {

                // 负载均衡选择一个生产者Invoker

                Invoker<T> retryInvoker = select(loadbalance, invocation, invokers, Collections.singletonList(lastInvoker));

                lastInvoker = retryInvoker;

                // 服务消费发起远程调用

                retryInvoker.invoke(invocation);

            } catch (Throwable e) {

                logger.error("Failed retry to invoke method " + invocation.getMethodName() + ", waiting again.", e);

                // 超出最大重试次数记录日志不抛出异常

                if ((++retryTimes) >= retries) {

                    logger.error("Failed retry times exceed threshold (" + retries + "), We have to abandon, invocation->" + invocation);

                } else {

                    // 未超出最大重试次数重新放入定时器

                    rePut(timeout);

                }

            }

        }

        privatevoidrePut(Timeout timeout){

            if (timeout == null) {

                return;

            }

            Timer timer = timeout.timer();

            if (timer.isStop() || timeout.isCancelled()) {

                return;

            }

            timer.newTimeout(timeout.task(), tick, TimeUnit.SECONDS);

        }

    }

}

2.2.5 Forking

并行调用策略。消费者通过线程池并发调用多个生产者,只要有一个成功就算成功:

public classForkingClusterInvokerextendsAbstractClusterInvoker{

    private final ExecutorService executor = Executors.newCachedThreadPool(new NamedInternalThreadFactory("forking-cluster-timer", true));

    publicForkingClusterInvoker(Directory<T> directory){

        super(directory);

    }

    @Override

    publicResultdoInvoke(finalInvocation invocation, List> invokers, LoadBalance loadbalance)throwsRpcException{

        try {

            checkInvokers(invokers, invocation);

            final List<Invoker<T>> selected;

            // 获取配置参数

            final int forks = getUrl().getParameter(Constants.FORKS_KEY, Constants.DEFAULT_FORKS);

            final int timeout = getUrl().getParameter(Constants.TIMEOUT_KEY, Constants.DEFAULT_TIMEOUT);

            // 获取并行执行的Invoker列表

            if (forks <= 0 || forks >= invokers.size()) {

                selected = invokers;

            } else {

                selected = new ArrayList<>();

                for (int i = 0; i < forks; i++) {

                    // 选择生产者

                    Invoker<T> invoker = select(loadbalance, invocation, invokers, selected);

                    // 防止重复增加Invoker

                    if (!selected.contains(invoker)) {

                        selected.add(invoker);

                    }

                }

            }

            RpcContext.getContext().setInvokers((List) selected);

            final AtomicInteger count = new AtomicInteger();

            final BlockingQueue<Object> ref = new LinkedBlockingQueue<>();

            for (final Invoker<T> invoker : selected) {

                // 在线程池中并发执行

                executor.execute(new Runnable() {

                    @Override

                    publicvoidrun(){

                        try {

                            // 执行消费逻辑

                            Result result = invoker.invoke(invocation);

                            // 存储消费结果

                            ref.offer(result);

                        } catch (Throwable e) {

                            // 如果异常次数大于等于forks参数值说明全部调用失败,则把异常放入队列

                            int value = count.incrementAndGet();

                            if (value >= selected.size()) {

                                ref.offer(e);

                            }

                        }

                    }

                });

            }

            try {

                // 从队列获取结果

                Object ret = ref.poll(timeout, TimeUnit.MILLISECONDS);

                // 如果异常类型表示全部调用失败则抛出异常

                if (ret instanceof Throwable) {

                    Throwable e = (Throwable) ret;

                    throw new RpcException(e instanceof RpcException ? ((RpcException) e).getCode() : 0, "Failed to forking invoke provider " + selected + ", but no luck to perform the invocation. Last error is: " + e.getMessage(), e.getCause() != null ? e.getCause() : e);

                }

                return (Result) ret;

            } catch (InterruptedException e) {

                throw new RpcException("Failed to forking invoke provider " + selected + ", but no luck to perform the invocation. Last error is: " + e.getMessage(), e);

            }

        } finally {

            RpcContext.getContext().clearAttachments();

        }

    }

}

2.2.6 Broadcast

广播调用策略。消费者遍历调用所有生产者节点,任何一个出现异常则抛出异常:

public classBroadcastClusterInvokerextendsAbstractClusterInvoker{

    private static final Logger logger = LoggerFactory.getLogger(BroadcastClusterInvoker.class);

    publicBroadcastClusterInvoker(Directory<T> directory){

        super(directory);

    }

    @Override

    publicResultdoInvoke(finalInvocation invocation, List> invokers, LoadBalance loadbalance)throwsRpcException{

        checkInvokers(invokers, invocation);

        RpcContext.getContext().setInvokers((List) invokers);

        RpcException exception = null;

        Result result = null;

        // 遍历调用所有生产者节点

        for (Invoker<T> invoker : invokers) {

            try {

                // 执行消费逻辑

                result = invoker.invoke(invocation);

            } catch (RpcException e) {

                exception = e;

                logger.warn(e.getMessage(), e);

            } catch (Throwable e) {

                exception = new RpcException(e.getMessage(), e);

                logger.warn(e.getMessage(), e);

            }

        }

        // 任何一个出现异常则抛出异常

        if (exception != null) {

            throw exception;

        }

        return result;

    }

3 怎么做幂等

经过上述分析我们知道,RPC框架自带的重试机制可能会造成数据重复问题,那么在使用中必须考虑幂等性。幂等性是指一次操作与多次操作产生结果相同,并不会因为多次操作而产生不一致性。常见幂等方案有取消重试、幂等表、数据库锁、状态机这些方案。

3.1 取消重试

取消重试有两种方法,第一是设置重试次数为零,第二是选择不重试的集群容错策略。


3.2 幂等表

假设用户支付成功后,支付系统将支付成功消息,发送至消息队列。物流系统订阅到这个消息,准备为这笔订单创建物流单。

但是消息队列可能会重复推送,物流系统有可能接收到多次这条消息。我们希望达到效果:无论接收到多少条重复消息,只能创建一笔物流单。

解决方案是幂等表方案。新建一张幂等表,该表就是用来做幂等,无其它业务意义,有一个字段名为key建有唯一索引,这个字段是幂等标准。

物流系统订阅到消息后,首先尝试插入幂等表,订单编号作为key字段。如果成功则继续创建物流单,如果订单编号已经存在则违反唯一性原则,无法插入成功,说明已经进行过业务处理,丢弃消息。

这张表数据量会比较大,我们可以通过定时任务对数据进行归档,例如只保留7天数据,其它数据存入归档表。

还有一种广义幂等表就是我们可以用Redis替代数据库,在创建物流单之前,我们可以检查Redis是否存在该订单编号数据,同时可以为这类数据设置7天过期时间。

3.3 状态机

物流单创建成功后会发送消息,订单系统订阅到消息后更新状态为完成,假设变更是将订单状态0更新至状态1。订单系统也可能收到多条消息,可能在状态已经被更新至状态1之后,依然收到物流单创建成功消息。

解决方案是状态机方案。首先绘制状态机图,分析状态流转形态。例如经过分析状态1已经是最终态,那么即使接收到物流单创建成功消息也不再处理,丢弃消息。

3.4 数据库锁

数据库锁又可以分为悲观锁和乐观锁两种类型,悲观锁是在获取数据时加锁:


乐观锁是在更新时加锁,第一步首先查出数据,数据包含version字段。第二步进行更新操作,如果此时记录已经被修改则version字段已经发生变化,无法更新成功:


分析了为什么重试这个问题,因为对于RPC交互无响应场景,重试策略是一种重要选择。然后分析了DUBBO提供的六种集群容错策略,Failover作为默认策略提供了重试机制,在业务代码没有显示重试情况下,仍有可能发起多次调用,这必须引起重视。

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

推荐阅读更多精彩内容

  • 前言 本文继续分析dubbo的cluster层,此层封装多个提供者的路由及负载均衡,并桥接注册中心,以Invoke...
    Java大生阅读 991评论 0 0
  • 在介绍dubbo的cluster之前,先来看一下cluster在dubbo整体设计中的位置。按照官网的说法,Clu...
    花老湿不闷骚阅读 805评论 0 1
  • 先看官网一张图image.png这就是dubbo的集群设计了,本章主要解析的就是图中的主要几个蓝色点,简单堆土做个...
    致虑阅读 390评论 0 1
  • 我是黑夜里大雨纷飞的人啊 1 “又到一年六月,有人笑有人哭,有人欢乐有人忧愁,有人惊喜有人失落,有的觉得收获满满有...
    陌忘宇阅读 8,532评论 28 53
  • 信任包括信任自己和信任他人 很多时候,很多事情,失败、遗憾、错过,源于不自信,不信任他人 觉得自己做不成,别人做不...
    吴氵晃阅读 6,187评论 4 8