当周杰伦把QQ音乐干翻的时候,作为程序猿我看到了什么?

本文分析了周杰伦新歌《说好不哭》发布导致QQ音乐服务器崩溃的技术原因,探讨了缓存击穿、穿透与雪崩现象及应对策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

点击蓝字关注我吧



1

别人都会唱了,而我还没付钱!



        2019年9月16日晚23点整,周杰伦发布新歌《说好不哭》。

        我经过一系列猛如虎的操作:

640?wx_fmt=jpeg


        咦!这啥?

640?wx_fmt=jpeg

        发生错误了?what the fuck!

640?wx_fmt=gif

        虽然说好不哭,但是还没开始听之前我就哭了:
        再等二分钟,别人都会唱了,我还没付钱!不要这样吧!QQ音乐你要振作起来啊!

1

知识结构的差别

导致我们看的角度不同



        不知道大家有没有看过美剧《越狱》。
        引用《奇葩说》辩手,也是我的男神,陈铭说的一段话:

主角迈克进入监狱救他哥哥,他走进了监狱,看到了那所监狱。

那一瞬间,我才发现迈克是个建筑学家。

他看到的监狱和我看到的监狱根本不是同一个监狱,。

我看到的是囚牢、操场、移动的犯人和狱卒。 

而迈克一走进去,他看到的是通风管道、下水管道、紧急通道,他看到了墙后面所有的东西。

我这时意识到了一点,建筑学家跟我们因为知识背景和知识框架的不同,我们看到的是两个不一样的世界。 

这是知识结构的差别带来的我们眼睛看到的世界的截然不同。

        我举这个例子想要说明的是,当我站在程序员的角度看QQ音乐崩了这件事情的时候,我看到了什么,我想到了什么,这是一个由无数服务器、若干微服务、负载均衡、多级缓存、巨大流量、分库分表、读写分离、搜索引擎、性能优化、高速硬盘......组成的世界。

        周杰伦站在世界的这头,手机不停的响着:"微信到账三元"。
        程序员站在世界的这头,嘴里不停的喊着:"马上撑不住了,快降级、加机器、部服务"。

640?wx_fmt=jpeg

中间的架构图是我随便找的,和QQ音乐无关

1

正文开始



        好了,当顶级流量周杰伦把QQ音乐干翻的时候,我作为程序猿看到了什么?且听我细细道来。

        当我点击立即购买,没有弹出支付页面,而是弹出"发生错误了"的提示,如果那一晚你也在关注周董的新歌,我相信你和我做了同样的动作:马上关闭了页面,再次点击了立即购买,但是还是弹出错误提示。那个时候,我才反应过来,哦,原来是QQ音乐崩了。

        这个时候我的脑海里面立即浮现出了一个由请求,redis缓存,数据库组成画面。

640?wx_fmt=png

        这图,是我这个灵魂画手亲手画的,当然还是和QQ音乐的架构没有半毛钱关系。甚至QQ音乐这次崩掉也许和缓存也没有半毛钱关系。但是我就是想到了这个画面。
        熟知redis的小伙伴一看到这里,下意识的就会说:"哟,这不是 缓存击穿吗?"
        不知道缓存击穿的小伙伴,不要着急。看完这篇文章后,你不仅懂了缓存击穿,还会懂缓存穿透,缓存雪崩,以及对应的解决方案。

        熟知redis,并且对这几个概念烂熟于心的小伙伴这个时候可能想要走了,没关系,答应我, 走之前,拉到最后点个"在看"。 谢谢!

        再开始之前,我想多说一句话,垫个底:
        为什么我们要用缓存?
        其中大部分的原因是为了提高系统的响应速度,提升并发访问量。因为从内存中,比如redis读取数据和从磁盘中,比如mysql读取数据的响应速度是不在一个级别的。
        我给你打个形象但不是很恰到的比方吧:就类似于光速和音速的差距。

1

缓存击穿



缓存击穿的概念

        缓存击穿是指一个请求要访问的数据,缓存中没有,但数据库中有。
这种情况一般来说就是缓存过期了。但是这时由于并发访问这个缓存的用户特别多,这是一个热点key,这么多用户的请求同时过来,在缓存里面都没有取到数据,所以又同时去访问数据库取数据,引起数据库流量激增,压力瞬间增大,直接崩溃给你看。

        所以一个数据有缓存,每次请求都从缓存中快速的返回了数据,但是某个时间点缓存失效了,某个请求在缓存中没有请求到数据,这时候我们就说这个请求就"击穿"了缓存。

640?wx_fmt=png

缓存击穿

缓存击穿的解决方案

方案一 互斥锁
        互斥锁方案的思路就是如果从redis中没有获取到数据,就让一个线程去数据库查询数据,然后构建缓存,其他的线程就等着,过一段时间后再从redis中去获取。

640?wx_fmt=png


        伪代码如下:
String get(String jay) {
   String music = redis.get(jay);
   if (music == null) {
   //nx的方式设置一个key=jay_lock,
   //value=aiwobieku_lock的数据,60秒后过期
    if (redis.set("jay_lock", "aiwobieku_lock","nx",60)) {
        //从数据库里获取数据
        music = db.query(jay);
        //构建数据,24*60*60s后过期
        redis.set(jay, music,24*60*60);
        //构建成功,可以删除互斥锁
        redis.delete("jay_lock");
    } else {
        //其他线程休息100ms后重试
        Thread.sleep(100);
        //再次获取数据,如果前面在100ms内设置成功,则有数据
        music = redis.get(jay);
    }
  }
}

        这个方案能解决问题,但是一个线程构建缓存的时候,另外的线程都在睡眠或者轮询。
        而且在这个四处宣讲高并发,低延时的时代,你居然让你的用户等待了宝贵的100ms。有可能别人比你快100ms,就抢走了大批用户。
你说,你是何居心?是不是敌人派来的卧底?

方案二 后台续命
        后台续命方案的思想就是, 后台开一个定时任务,专门主动更新即将过期的数据。
        比如程序猿设置jay这个热点key的时候,同时设置了过期时间为60分钟,那后台程序在第55分钟的时候,会去数据库查询数据并重新放到缓存中,同时再次设置缓存为60分钟。

        呃,这个方案呢。怎么说呢,我感觉很奇怪。
        可能是没有想到合适的应用场景,而且觉得代码实现起来比较复杂。

        但是这种思想是没问题的,我之前就借助这样的思想开发过一个功能:
简单的描述一下就是:
        流水号系统,采用数据库自增主键来保证唯一性,但是属于非常关键的系统,为了降低数据库异常对服务带来的冲击,所以服务启动后会就会预先在缓存中缓存5000个流水号。然后后台job定时检查缓存中还剩下多少流水号,如果小于1000个,则再从数据库中生成流水号,补充到缓存中,让缓存中的流水号再次回到5000个。这样做的好处就是数据库异常后,我至少保证还有5000个缓存可以保证上游业务,我有一定的时间去恢复数据库。

640?wx_fmt=png

后台续命的另一种展示


方案三 永不过期
        这个方案就有点简单粗暴了。
        见名知意,如果结合实际场景你用脚趾头都能想到这个key是一个热点key,会有大量的请求来访问这个数据。对于这样的数据你还设置过期时间干什么?直接放进去,永不过期。

640?wx_fmt=png

永不过期

        这个热点流量就类似于
周董发新歌,
鹿晗爱晓彤,
唱跳rap和篮球的流量。
        比起产品经理给你提需求,让你开发的时候,给你预报的流量靠谱多了。
        我就遇见过产品经理来提需求的时候说:
这个需求特别急,最好明天就上线。
上线流量马上来,你的系统要抗住。
        结果往往是:
熬夜加班通宵干,终于爆肝弄上线。
结果上线没动静,他说商户不接了。
        大道至简,我个人喜欢这个方案。

        但是具体情况具体分析,没有一套方案走天下的。
        比如,如果这个key是属于被各种"自来水"活生生的炒热的呢?就像哪吒一样,你预想不到这个key会闹出这么大的动静。这种情况你这么处理?
        所以,具体情况,具体分析。但是思路要清晰,最终方案都是常规方案的组合或者变种。

1

缓存穿透



        缓存穿透是指一个请求要访问的数据, 缓存和数据库中都没有,而用户短时间、高密度的发起这样的请求,每次都打到数据库服务上,给数据库造成了压力。一般来说这样的请求属于恶意请求,

640?wx_fmt=png

缓存穿透

        根据图片显示的,缓存中没有获取到数据,然后去请求数据库,没想到数据库里面也没有。
比如明明是周杰伦的演唱会,你冲过保安大哥,上台对周董说:"给我来个林俊杰的签名"。
最可恶的是你也知道,周杰伦那里没有林俊杰的签名。
恶意请求,占用资源。当有成千上万这样的恶意请求的时候,你不做处理,就会给周杰伦,哦不,数据库带来压力。

缓存穿透的解决方案

方案一 --- 缓存空对象
        缓存空对象就是在数据库即使查到的是空对象,我们也把这个空对象缓存起来。

640?wx_fmt=png

缓存空对象

        下次同样请求就会命中这个空对象,缓存层就处理了这个请求,不会对数据库产生压力。

        这样实现起来简单,开发成本很低。但这样随之而来的两个面试题必须要注意一下:
第一个问题:如果在某个时间,缓存为空的记录,在数据库里面有值了,你怎么办?
        我知道三个解决方法:
解决方法一:设置缓存的时候,同时设置一个过期时间,这样过期之后,就会重新去数据库查询最新的数据并缓存起来。
解决方法二:如果对实时性要求非常高的话,那就写数据库的时候,同时写缓存。这样可以保障实时性。
解决方法三:如果对实时性要求不是那么高,那就写数据库的时候给消息队列发一条数据,让消息队列再通知处理缓存的逻辑去数据库取出最新的数据。
        另外说明一下:对于数据库和缓存一致性的问题,我不打算在这篇文章讨论。个人感觉这是一个引战的问题。后面会单独介绍我自己对于这个问题的看法。请大神不要在这"开杠"。
第二个问题:对于恶意攻击,请求的时候key往往各不相同,且只请求一次,那你要把这些key都缓存起来的话,因为每个key都只请求一次,那还是每次都会请求数据库,没有保护到数据库呀?
        这个时候,你就告诉他:"布隆过滤器,了解一下"。
    
方案二 --- 布隆过滤器
        什么是布隆过滤器?

本质上布隆过滤器是一种数据结构,比较巧妙的概率型数据结构(probabilistic data structure),特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”。 

相比于传统的 List、Set、Map 等数据结构,它更高效、占用空间更少,但是缺点是其返回的结果是概率性的,而不是确切的。

640?wx_fmt=png

        当布隆过滤器说某个值存在时,这个值可能不存在; 当它说不存在时,那就肯定不存在。
        所以布隆过滤器返回的结果是概率性的,所以它能缓解数据库的压力,并不能完全挡住,这点必须要明确。

        guava组件可以开箱即用的实现一个布隆过滤器,但是guava是基于内存的,所以不太适用于分布式环境下。
    
        要在分布式环境下使用布隆过滤器,那还得redis出马,redis可以用来实现布隆过滤器。
看到了吗,redis不仅仅是拿来做缓存的。这就是一个知识点呀。
        什么?你想看他是怎么实现的?对不起,我也只是知道并且会用它,内部原理我还说不太清楚,你可以自行查阅一下。所以:

640?wx_fmt=jpeg


1

缓存雪崩



        缓存雪崩是指缓存中 大多数的数据在同一时间到达过期时间,而查询数据量巨大,这时候,又是 缓存中没有,数据库中有的情况了。请求都打到数据库上,引起数据库流量激增,压力瞬间增大,直接崩溃给你看

640?wx_fmt=jpeg

        和前面讲的缓存击穿不同的是,缓存击穿指大量的请求并发查询同一条数据。
        缓存雪崩是不同数据都到了过期时间,导致这些数据在缓存中都查询不到,

640?wx_fmt=png


        或是缓存服务直接挂掉了,所以缓存都没有了。

640?wx_fmt=png

        总之,请求都打到了数据库上。对于数据库来说,流量雪崩了,很形象。

缓存雪崩的解决方案

方案一 --- 加互斥锁
        如果是大量缓存在同一时间过期的情况,那么我们可以加互斥锁。
        等等,互斥锁不是前面介绍过了吗?
        是的,缓存雪崩可以看成多个缓存击穿,所以也可以使用互斥锁的解决方案,这里就不再赘述。

方案二 --- "错峰"过期
        如果是大量缓存在同一时间过期的情况,我们还有一种解决方案就是在设置key过期时间的时候,在加上一个短的随机过期时间,这样就能避免大量缓存在同一时间过期,引起的缓存雪崩。
比如设置一类key的过期时间是10分钟,在10分钟的基础上再加上60秒的随机事件,就像这样:
redis.set(key,value,10*60+RandomUtils.nextInt(0, 60),TimeUnit.SECONDS)

方案三 --- 缓存集群
        如果对于缓存服务挂掉的情况,大多原因是单点应用。那么我们可以引入redis集群,使用主从加哨兵。用Redis Cluster部署集群很方便的,可以了解一下。
        当然这是属于一种事前方案,在使用单点的时候,你就得考虑服务宕机后引起的问题。所以,事前部署集群,提高服务的可用性。
   
方案四 --- 限流器+本地缓存
        那你要说如果Cluster集群也挂了怎么办呢?

640?wx_fmt=jpeg

        如果你能层层深入考虑到集群也挂了怎么办的话,那你可真是一个爱思考的好同学。其实就是对服务鲁棒性的考虑:

鲁棒性(robustness)就是系统的健壮性。它是指一个程序中对可能导致程序崩溃的各种情况都充分考虑到,并且作相应的处理,在程序遇到异常情况时还能正常工作,而不至于死机。

        这个时候,可以考虑一下引入限流器,比如  Hystrix,然后实现服务降级。
        假设你的限流器设置的一秒钟最多5千个请求,那么这个时候来了8千个请求,多出来的3000个就走降级流程,对用户进行友好提示。
        进来的这5000个请求,如果redis挂了,还是有可能会干翻数据库的,那么这个时候我们在加上如果redis挂了,就查询类似于echcache或者guava cache本地缓存的逻辑,则可以帮助数据库减轻压力,挺过难关。

方案五 --- 尽快恢复
        这个没啥说的了吧?
        大哥,你服务挂了诶?赶紧恢复服务啊。
        这个时候就涉及到redis的持久化和恢复数据的逻辑了,这里由于篇幅关系,就不展开讲述了,也是知识点啊,朋友们,全是知识点啊。

640?wx_fmt=jpeg

        但是你要说这是一个解决方案,我自己都觉得有点牵强,主要意思你懂的吧?尽快,争分夺秒的恢复数据。
        至于是勇敢承担还是积极甩锅的事,恢复后再慢慢考虑。

1

缓存之外



        据官方数据,周杰伦《说好不哭》发售不到半小时,销量200万张!

640?wx_fmt=png

        由于我之前是做支付相关开发的,从程序猿的角度,不仅看到了白花花的银子,还去算了一下平均每秒的销售量。
2000000/30/60=1111
        约等于每秒1111张,一张就是一笔交易,按照规律,我保守猜测,在刚刚开始发售的时候,请求量至少是平均数的3倍吧。那么就是一秒约3500笔的交易。每一笔都是疯狂的写请求,再加上这期间大量的评论和转发。有可能这才是导致QQ音乐崩溃的诱因。

1

总结



       前面介绍了缓存击穿,缓存穿透,缓存雪崩的场景和其对应的各种解决方案。 但是不同的解决方案有不同的适用场景和优缺点,你需要仔细权衡自己的需求之后妥善适用它们。
        

640?wx_fmt=gif

      先学习方案的思想,融会贯通之后,你就能触类旁通。
      写代码难吗?不难。相反应该是整个开发过程中最简单的一步。难的是你要理解需求,了解场景,拿出对应的解决方案。解决方案都有了,代码不就是呼之欲出吗?
      到了写代码的这一步了,难的不是实现需求,难的是你怎么优雅的实现需求。优雅,你懂的吧?程序猿的自我修养之一。

      最后,还是之前说的:
知识结构的差别,带来的我们眼睛看到的世界的截然不同。
      以上,是我个人看到周杰伦凭一首单曲,把QQ音乐干翻之后的一些思考和感悟。个人拙见,不足之处,欢迎大家指出问题。

1

表白这个男人



      最后,放一段我2014年,年终总结中的一段话吧:

640?wx_fmt=png

     表白这个男人。
    《说好不哭》,你可以说不好听。那是你的话语权,我捍卫你说话的权利。但是我不接受这个观点,这是我的权利。
      或者说,这是我和我身边大多数人的青春。

      完结撒花,下周再见。
      谢谢大家的阅读,如果觉得还不错的话,欢迎关注,再看并转发哦。
      

640?wx_fmt=png

往期精彩回顾




     
640

图:why技术

文:why技术

排版:why技术

扫码查看更多内容

640?wx_fmt=png 

640

内容概要:本文详细探讨了制造业工厂中两条交叉轨道(红色和紫色)上的自动导引车(AGV)调度问题。系统包含2辆红色轨道AGV和1辆紫色轨道AGV,它们需完成100个运输任务。文章首先介绍了AGV系统的背景和目标,即最小化所有任务的完成时间,同时考虑轨道方向性、冲突避免、安全间隔等约束条件。随后,文章展示了Python代码实现,涵盖了轨道网络建模、AGV初始化、任务调度核心逻辑、电池管理和模拟运行等多个方面。为了优化调度效果,文中还提出了冲突避免机制增强、精确轨道建模、充电策略优化以及综合调度算法等改进措施。最后,文章通过可视化与结果分析,进一步验证了调度系统的有效性和可行性。 适合人群:具备一定编程基础和对自动化物流系统感兴趣的工程师、研究人员及学生。 使用场景及目标:①适用于制造业工厂中多AGV调度系统的开发与优化;②帮助理解和实现复杂的AGV调度算法,提高任务完成效率和系统可靠性;③通过代码实例学习如何构建和优化AGV调度模型,掌握冲突避免、路径规划和电池管理等关键技术。 其他说明:此资源不仅提供了详细的代码实现和理论分析,还包括了可视化工具和性能评估方法,使读者能够在实践中更好地理解和应用AGV调度技术。此外,文章还强调了任务特征分析的重要性,并提出了基于任务特征的动态调度策略,以应对高峰时段和卸载站拥堵等情况。
内容概要:本文介绍了一个使用MATLAB编写的基于FDTD(时域有限差分)方法的电磁波在自由空间中传播的仿真系统。该系统采用了ABC(吸收边界条件)和正弦脉冲激励源,并附有详细的代码注释。文中首先介绍了关键参数的选择依据及其重要性,如空间步长(dx)和时间步长(dt),并解释了它们对算法稳定性和精度的影响。接着阐述了电场和磁场的初始化以及Yee网格的布局方式,强调了电场和磁场分量在网格中的交错排列。然后详细讲解了吸收边界的实现方法,指出其简单而有效的特性,并提醒了调整衰减系数时需要注意的问题。最后,描述了正弦脉冲激励源的设计思路,包括脉冲中心时间和宽度的选择,以及如何将高斯包络与正弦振荡相结合以确保频带集中。此外,还展示了时间步进循环的具体步骤,说明了磁场和电场分量的更新顺序及其背后的物理意义。 适合人群:对电磁波传播模拟感兴趣的科研人员、高校学生及工程技术人员,尤其是那些希望深入了解FDTD方法及其具体实现的人群。 使用场景及目标:适用于教学演示、学术研究和技术开发等领域,旨在帮助使用者掌握FDTD方法的基本原理和实际应用,为后续深入研究打下坚实基础。 阅读建议:由于本文涉及较多的专业术语和技术细节,建议读者提前熟悉相关背景知识,如电磁理论、MATLAB编程等。同时,可以通过动手实践代码来加深理解和记忆。
### 使用Python爬虫抓取QQ音乐网站上周杰伦的歌曲信息 为了实现这一目标,可以采用`requests`库配合`BeautifulSoup`解析网页内容。然而,由于现代Web应用广泛使用JavaScript动态加载数据,直接通过简单的HTTP GET请求可能无法获得所需的数据。对于这种情况,通常有两种解决方案: #### 方案一:分析AJAX请求并模拟发送 许多网站在前端展示的内容实际上是通过向服务器发出额外的API调用来获取的。这些API往往返回JSON格式的数据,可以直接用于提取有用信息而无需处理HTML文档。 针对QQ音乐的情况,在浏览器开发者工具中查看网络活动可以帮助找到实际查询歌曲列表所使用的接口地址及其参数设置方式[^1]。一旦确定了具体的URL模式以及必要的查询字符串参数(如页码、关键词等),就可以利用`requests`构建相应的GET请求来取得每一页的结果集,并从中解析出感兴趣的字段,比如歌曲名称、专辑封面链接等等。 ```python import json import requests def fetch_songs(page_num): url = "https://c.y.qq.com/soso/fcgi-bin/client_search_cp" params = { 'ct': '24', 'qqmusic_ver': '1298', 'new_json': '1', 'remoteplace': 'sizer.yqq.song_next', 'searchid': '64405487069162918', 't': '0', 'aggr': '1', 'cr': '1', 'catZhida': '1', 'lossless': '0', 'flag_qc': '0', 'p': str(page_num), 'n': '20', # Number of songs per page 'w': '%E5%91%A8%E6%9D%B0%E4%BC%A6', # URL-encoded search term (Jay Chou) 'g_tk_new_20200303': '5381', 'g_tk': '5381', 'loginUin': '0', 'hostUin': '0', 'format': 'json', 'inCharset': 'utf8', 'outCharset': 'utf-8', 'notice': '0', 'platform': 'yqq.json', 'needNewCode': '0' } response = requests.get(url=url, headers={'Referer': 'https://y.qq.com/'}, params=params) if response.status_code != 200: raise Exception(f"Failed to retrieve data with status code {response.status_code}") result = json.loads(response.content.decode()) song_list = [] try: for item in result["data"]["song"]["list"]: title = item["name"] album_name = item["album"]["name"] singer_names = ",".join([singer["name"] for singer in item["singer"]]) song_info = {"title": title, "albumName": album_name, "artistNames": singer_names} song_list.append(song_info) return song_list except KeyError as e: print("Error parsing JSON:", e) for i in range(1, 11): # Fetch first ten pages songs_on_page_i = fetch_songs(i) for song in songs_on_page_i: print(json.dumps(song)) ``` 此脚本定义了一个名为`fetch_songs()`的功能函数,该函数接受一个整数类型的参数表示要访问的具体页面编号;接着设置了包含必要查询项在内的字典对象作为GET方法中的参数传递给指定的目标站点;最后对响应体内的结构化数据进行了适当转换以便于后续操作。 请注意,上述代码片段仅适用于说明目的,具体实施时还需要考虑更多细节问题,例如错误处理机制的设计、反爬策略规避措施的应用等方面。 #### 方案二:借助自动化测试框架Selenium 如果遇到某些情况下难以定位到确切的API端点或者对方采取了一定程度上的防护手段使得常规手段失效,则可转而运用像Selenium这样的工具来进行交互式的浏览行为仿真。这种方式虽然效率较低且资源消耗较大,但在面对复杂场景时却能提供更高的灵活性和成功率。 安装依赖包之后,可以通过下面这段示范性的代码完成相似的任务: ```python from selenium import webdriver from time import sleep driver_path = '/path/to/chromedriver' # Replace this path according to your environment setup. options = webdriver.ChromeOptions() prefs = {'profile.managed_default_content_settings.images': 2} # Disable image loading to speed up the process options.add_experimental_option('prefs', prefs) browser = webdriver.Chrome(executable_path=driver_path, options=options) try: base_url = f"https://y.qq.com/n/ryqq/search?page={i}&query=%E5%91%A8%E6%9D%B0%E4%BC%A6&type=song" browser.get(base_url.format(i)) while True: soup = BeautifulSoup(browser.page_source, features="lxml") tracks = [] track_elements = soup.select('.result__item') for element in track_elements[:len(track_elements)]: name = element.h4.a.string.strip().replace('\n', '') artist = ''.join(element.span.stripped_strings).strip() tracks.append({ 'trackTitle': name, 'artists': artist }) for t in tracks: print(t) next_button = browser.find_element_by_css_selector(".btn-next") if not ('disabled' in next_button.get_attribute('class')): next_button.click() sleep(2) # Wait until new content loads completely before continuing else: break finally: browser.quit() # Ensure that WebDriver instance is closed properly even when an exception occurs during execution. ``` 这里选择了ChromeDriver作为驱动器实例化了一个WebDriver对象,并配置了一些选项以优化性能表现。随后进入循环迭代过程直至遍历完所有的分页记录为止。每次读取当前视图下的DOM树节点集合后即刻执行下一步动作直到找不到可用的“下一页”按钮位置才停止整个流程。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值