黑马点评项目要点内容总结【面试用】

1.使用Redis代替Session登录

1.1 Session登录存在的问题

  • Session的数据都保存在服务器中数据量很大的时候可能导致服务器内存不足
  • 在分布式系统下每一个服务器的Session数据是独立的用户在不同服务器之间切换的时候可能因为session问题而需要反复登录

1.2 使用Redis代替Session登录分析

为什么可以是用Redis代替Session

  • Redis是基于内存的读写速度非常快和Session类似
  • 多个服务器访问的是同一个Redis就实现了数据的共享。此外Redis集群内部的数据一致性机制也很棒

Redis中数据结构和key的选择

  • 保存验证码到Redis由于在登录中还需要根据用户手机号来从Redis中获取验证码并比对并且保存到信息相对简单因此采用的key就是手机号value保存验证码。采用String类型保存。
  • 登录key设计登录key需要满足脱敏性和唯一性。因此使用UUID作为keyvalue是经过脱敏后的用户对象JSON字符串。这里没有使用Hash存储主要因为保存的数据相对简单并且没有对用户中某一个属性进行访问的需求。

1.3 使用Redis登录的流程

发送验证码流程

  • 验证手机号
  • 获取验证码
  • 将验证码保存到Redis中。key为手机号

登录流程

  • 验证手机号
  • 获取用户输入验证码并根据手机号从Redis中获取验证码进行比较
  • 通过后生成UUID的token。将用户信息脱敏后以token为key保存到Redis中
  • 向客户端返回token

1.4 解决Redis中有效期问题

存在的问题

根据上述逻辑Redis中保存用户信息的记录只在用户登录时设置了30min。用户访问其他网页时记录并不会向session一样续期。

  • 方案一在登录拦截器中从Redis中获取到用户信息然后续期。但是对于这个项目来说查看商店信息等请求并不会被拦截因为不登录也可以看。当登录用户访问这些请求的时候Redis并不会续期导致记录过期。
  • 方案二在登录拦截器之间加一个全局拦截器这个拦截器拦截所有请求并放行所有请求。它主要的作用时判断用户是否登录如果登录则续期。

2.使用Redis作为缓存

2.1 为什么使用缓存

在高并发的场景下用户对于一些热点数据如商品信息等访问请求量很大如果每一次请求都访问数据库那么对数据库等压力是非常大的。因此考虑使用缓存。

2.2 缓存策略

考虑了使用缓存就不得不谈缓存和数据库的数据一致性问题。在这个项目中采用了在代码中手动更新的方式来保证数据的一致性。下面还有一些细节问题

问题1: 当数据库中的数据发生变化的时候是删除缓存还是更新缓存

应该是删除缓存。因为当数据库中的某一个数据发生多次变化而在这期间没有请求访问数据库那么更新缓存的操作只有最后一次有效因此应该选择删除缓存。当有访问请求的时候再去重建缓存。

问题2: 如何保证缓存和数据库的操作同时成功

在单体项目中使用事务注解在分布式中使用分布式事务框架

问题3: 先操作数据库还是先操作缓存

应该是先操作数据库。因为如果先操作缓存那么把缓存删除掉此时有一个请求访问缓存发现缓存中没有数据那么就访问数据库重建缓存而此时数据库还没有进行修改。当数据修改完成后又有新的请求访问一看缓存中有那么直接读取之间的脏数据。

2.3 解决缓存穿透

什么是缓存穿透

一个请求始终访问数据库和缓存中都没有的记录导致每一次请求都需要到数据库中进行查询给数据库造成巨大压力。这种现象就是缓存穿透。

解决方案 本项目中采用缓存空对象的方法解决这个问题。

  • 当一个请求请求某一个数据然后查询到缓存中没有则到数据库查询
  • 如果数据库中也没有查询到则在缓存中创建一个空对象。
  • 这样当这个请求再次发送时缓存中就有了一个空对象数据。我们在查询缓存时判断如果从缓存中查询到的结果是空对象那么就直接返回查询不到即可。

这样就解决了缓存穿透的问题。但是这个方案还有一些缺点比如如果一开始请求的这个数据不存在后期存在了然而缓存中保存的还是空对象就导致这个新增的数据查询不到。我们可以通过合理的设置空对象的过期时间来缓解这个问题。

除此之外在项目中增加ID值的长度这样的话如果有人恶意的访问某一个不存在的数据在前端检测的时候直接发现ID不合法那么也可以避免对数据库的访问。

2.4 解决缓存雪崩

什么是缓存雪崩

缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机导致大量请求到达数据库带来巨大压力。、

解决方案给不同的Key的TTL添加随机值。保证key不会同时失效。利用Redis集群提高服务的可用性

2.5 解决缓存击穿

什么是缓存击穿

缓存击穿也叫做热点key问题。在高并发的情况下某一个热点key缓存过期导致大量的请求直接发送到数据库导致数据库压力过大。

比如说商品列表同时有很多人访问。一旦商品列表的缓存过期那么很多人的请求就直接打到数据库上数据库就很有可能出现宕机的情况。

解决方案1: 可以利用互斥锁的方式解决。

  • 当一个请求发现热点key过期以后直接获取互斥锁一旦获取到互斥锁那么就开始访问数据库然后重建缓存。
  • 其他的请求发现缓存过期但是已经有其他的请求获取到了互斥锁所以这个请求只能等待。
  • 当重建缓存的请求结束以后。其他的请求又发过来此时缓存已经建立完毕所以请求不会到数据库中。

这个互斥锁可以使用setnx命令来实现因为setnx命令只允许一个请求成功其他的都失败。

由于使用了互斥锁并发性会降低一些。

解决方案2: 使用逻辑过期的方式。

这种方案创建的热点key的TTL是-1。从而不会因为缓存失效而缓存击穿

  • 在缓存中保存数据的同时会保存一个逻辑过期时间的时间戳。
  • 当从缓存中查询到这个数据的时候首先获取它的逻辑过期时间。
  • 如果已经过期单独的去开辟一个线程用来重建数据。当前线程直接返回缓存中已经过期的数据。

这种方法的好处是保证了程序的并发性但是在某一段时间内程序读取到的数据是脏数据。

3.秒杀功能

3.1 全剧唯一ID生成器

用户在对优惠券抢购时一个订单就对应一个订单ID在高并发情况下需要保证ID的一下特点

  • 唯一性ID必须保证唯一
  • 高可用可以以极快的速度生成唯一ID
  • 递增型递增的ID有利于在数据库中建立索引

在本项目中自己实现了一个ID生成器。生成的ID结构如下

  • 第一位符号位永远为0
  • 时间戳位31bit以秒为单位可以使用69年。当前时间戳–自定义的起始时间戳
  • 32bit秒内的计数器支持每秒产生2^32个不同ID。这个使用Redis中的incr函数实现

Key的设计incr业务名日期

如果一个业务只建立一个key那么随着时间的推移redis中的value会达到上限此时ID生成器就不可用了。

3.2 秒杀业务V1.0 使用乐观锁解决超卖问题

超卖问题的产生

假设目前库存位为1在高并发环境下

  • 线程1执行判断是否有库存的操作然后时间片结束。结束的时候线程1并没有完成创建订单的操作。
  • 此时线程2获得时间片开始判断是否有库存由于线程1并没有完成下单操作因此此时库存仍然为1。
  • 这时线程1获得时间片完成下单操作。
  • 线程2获得时间片完成下单操作

那么就会出现超卖问题此时库存应该为-1

解决方案

目前解决方案有两种

  • 使用悲观锁在下单的业务上使用sync关键字。但是这样会导致秒杀业务变成串行执行严重降低并发性。
  • 使用乐观锁在更新库存的时候判断一下是否和查询库存的时候结果一样如果一样则说明数据没有被修改过则可以执行。如果库存和之前不一样则回滚。

使用乐观锁解决超卖问题

boolean success = seckillVoucherService.update()
    .setSql("stock= stock -1") //set stock = stock -1
    .eq("voucher_id", voucherId).eq("stock",voucher.getStock()).update();        

最开始的思路就是在更新库存的时候判断一下库存是不是和当初查询库存的时候一样如果一样就更新否则就不更新。

在测试的时候发现有很多的优惠卷没有卖出去。在还有库存的时候就提示库存不足。

问题的原因在于通过分析上述的逻辑可以得出假设同时有100个线程同时拿到了100个库存那么他们拿到的版本号就应该是相同的此时只能有一个线程扣减库存成功修改了版本号其余所有线程会因为版本号不一致而扣减失败。因此我们还需要进一步的优化。

问题分析以后我们发现实际上在更新库存的时候只需要判断库存不为空就可以满足不超卖的条件因此修改代码为

boolean success = seckillVoucherService.update()
            .setSql("stock= stock -1")
            .eq("voucher_id", voucherId).update().gt("stock",0); 
            //where id = ? and stock > 0

3.3 秒杀业务V1.1 实现一人一单秒杀

优惠卷的目的主要是引流如果一个人可以购买多张优惠卷那么就没有意义了。因此还需要完成一人一单的功能。

存在的问题

从理论上来说只需要在代码中判断完库存充足以后然后在判断一下用户是否下过单即可如果下过单则直接返回异常信息。但是在使用JMeter测试时发现依然可以一个用户下多个订单的情况。

产生这个问题的原因还是线程的安全问题。

  • 假设线程1和线程2都是同一个用户多次点击购买发出请求对应的线程。

  • 那么当线程1判断用户有购买资格的时候。线程2突然获得时间片那么线程2也判断用户是否下过单。

  • 由于线程1还没有下单因此线程2查询到用户也是有购买资格的。然后就继续执行下单操作。

  • 此时线程1获取时间片由于已经判断过是否有购买资格线程1直接执行下单操作这样就导致了一个人下单多次的情况。

解决方案

根据刚才解决超卖问题的经验那么解决这个问题也应该可以使用悲观锁或乐观锁但是实际上这个问题只能使用悲观锁。因为这一次时插入数据并不是更新数据因此不存在所谓的版本号也就无法使用乐观锁。

确定加锁范围

经过分析可以发现需要保证判断用户是否下过单和创建订单的操作要满足原子性。也就是说在判断完是否可以下单以后其他的线程不得访问这一部分代码。所以我们要把锁加在判断用户是否下过单和创建订单的逻辑上。

确定锁的粒度

通常情况下在非静态方法中sync的锁监视器都是this对象在代码中this指代应该是VoucherOrderServiceImpl而这个对象是一个bean由Spring创建并且是单粒的。因此如果在sync中使用this作为锁监视器那么所有线程都共享这一个锁监视器那么加锁部分的代码就变成了完全串行执行。

而实际上我们只需要同一个用户发出的不同线程串行执行不同用户的线程可以并发执行。因此不可以使用this作为锁监视器。这里我们考虑使用UserId作为锁监视器userId.toString().intern()通过这个代码把用户id变成常量这样相同的用户会共享一个锁监视器从而完成业务。

Spring事务管理与sync关键字

目前加锁的方法位于createVoucherOrder()中这个方法上面有声明式事物注解那么就有可能出现锁已经释放了但是事物还没有提交那么之中情况也会出现超卖的现象。

锁已经释放了说明其他的线程可以进来而此时还有提交事物也就是说订单还没有写入数据库此时进来道线程还是可以查询到该用户有购买资格那么就会再次下单导致一个人多次下单的问题。

因此需要先提交事务再释放锁。所以需要将这部代码抽取成一个方法然后从外部调用。

在这里插入图片描述

Spring实现事务方式

我们知道Spring中实现事务是通过动态代理来实现的也就是说Spring调用的实际上是VoucherOrderServiceImpl的代理类对象而在上面的代码中我们直接使用了this也就是说是直接调用的VoucherOrderServiceImpl中的方法会导致声明式事物失效。因此需要先获取到当前对象的代理类对象然后通过代理类对象对方法进行调用才可以使声明式事务生效。

在这里插入图片描述

3.4 秒杀业务V1.2 实现集群环境下一人一单

存在的问题

在上一个版本中提到的解决一人一单的方法是通过加sync代码块实现。但是这种方法在集群环境下不适用。主要原因是因为在集群环境下每一个服务器都是一个独立的JVM线程1和线程2属于同一个服务器那么他们之间可以使用sync代码块实现互斥访问但是线程3线程4位于另一台服务器。由于不同的JVM他们之间的锁监视器是不共享的因此线程12和线程34之间是一种并发的状态。那么一人一单的问题就不能得到保证。

解决方法

利用Redis实现一个分布式锁。需要满足一下条件

  • 利用Redis中的setnx命令并加以设置过期时间。
    • setnx满足互斥性多个线程执行只有一个返回true
    • 使用过期时间可以保证出现故障后锁依然可以释放不会产生死锁问题。
    • 利用redis集群来提高可用性。
  • 释放锁的时候需要判断当前的锁是不是自己加的只有当前的锁是自己加的才可以删除。
  • 删除锁的动作需要具备原子性因此我们使用了Lua脚本实现多条指令的原子性。

因此我们可以利用Redis构建一个分布式锁。

核心思想是利用了Redis的setnx方法当多个线程进入时只有一个线程能够执行setnx方法返回值为true其余线程因为key已经存在返回的都是false这就实现互斥。
另外当该用户的其他线程得到的结果是false的时候应该直接返回"一个用户只能下一单"的提示而不是继续等待。

  • 利用Redis中的setnx命令并加以设置过期时间。
    • setnx满足互斥性多个线程执行只有一个返回true
    • 使用过期时间可以保证出现故障后锁依然可以释放不会产生死锁问题。
    • 利用redis集群来提高可用性。
  • 释放锁的时候需要判断当前的锁是不是自己加的只有当前的锁是自己加的才可以删除。
  • 删除锁的动作需要具备原子性因此我们使用了Lua脚本实现多条指令的原子性。

Redis分布式锁的key value问题

从理论上来说key应该是业务名+用户id但是对value并没有什么要求。但是在实际情况下value需要设置的UUID+线程ID。具体原因如下

试想一下一种情况当线程1获取到锁在执行业务的时候花费时间很长导致锁自动超时释放。此时线程2获取到了锁正在执行业务。此时线程1的业务也执行完了直接释放锁。导致线程1把线程2的锁给释放了产生了线程安全的问题。

这个问题实际上就是一个线程删除了本来不属于自己的锁

我们的解决方法是在value里面保存UUID+线程ID使用UUID的目的是在集群环境下可能会存在线程ID相同的问题。这样在删除锁的时候线程需要先根据value值判断是不是自己加的锁如果不是则说明其他线程已经获取到锁那么自己执行的业务就应该回滚。如果是自己的锁那么可以直接释放。

此外判断锁是不是属于自己和删除锁这两个操作应该保证原子性否则如果一个线程已经判断完是自己的锁还没有删除的时候突然失去时间片导致锁自动释放。另外一个线程又获取到了锁。那么当原来的已经判断完是自己的锁的那么线程再次获取到时间片就会直接释放锁而不会再次判断是不是自己的锁所以还是会释放掉不属于自己的锁。

在这个项目中采用了Lua脚本的方式来保证判断锁和删除锁的原子性。

3.5 可重入锁的原理

上面我们实现的锁并不具备可重入的功能当一个线程获取到锁以后即使它再次获取锁也会被阻塞。实际上我们可以通过使用hash结构实现可重入锁。

底层采用Redis的哈希存储方式除了存储以 lock:order:userId作为key以字段名threadId值为statsu的变量作为值。当线程获取锁的时候当重入时会先判断一下当前获取锁的线程是不是threadid里面的线程如果是则status+1
当释放锁的时候首先判断是不是自己获取的锁。如果是将statsu-1然后判断status是不是为0如果此时为0则释放锁否则不释放锁。

3.6 Redisson分布式锁

  • 利用哈希结构实现重入
  • 利用看门狗机制实现续期
  • 利用信号量控制锁重试

3.7 Redission锁的MutiLock

问题为了提高redis的可靠性通常会搭建主从集群来扩展redis。试想一下下面的情况

  • 线程1获取锁redis执行写操作将锁从master上写入而由于redis主从之间同步信息是需要时间的主机上的信息还没有完全同步到从机上结果主机宕机了。
  • 此时根据哨兵机制会从从机上选择一个作为新的主机而新的主机上还没有保存之前的锁就造成了线程安全问题。

解决方案使用redisson中的MutiLock。
原理不在使用主从机制。而是所有的redis都是地位相同的节点。此时获取锁需要分别从3个redis结点中获取锁只有3个结点都写入锁成功才算获取到锁。

4.使用Lua脚本结合消息队列实现异步秒杀业务

问题分析

在之前的秒杀业务中我们发现需要多次访问数据库并且业务也是串行执行的。但是分析一下我们可以发现我们可以将业务拆分成两个子业务

  • 一个业务只负责判断是否有购买资格如果有购买资格则直接创建订单信息到消息队列。此时并没有真正的访问数据库创建订单因此效率会非常高。

  • 第二个业务开辟一个单独的线程从消息队列中读取数据保存的数据库。

第二个业务并不需要很高的即时性当第一个业务判断完用户有购买资格后直接返回通知用户下单成功即可。

具体实现

  • 首先这些优惠券的热点信息包括库存等信息需要提前保存的Redis中
  • 在Redis中一个优惠券对应一个键value保存库存量
  • 在Redis中一个优惠券对应一个set集合里面保存不可重复列表列表中每一个元素保存下单成功的用户id
  • 用户下单可以直接访问Redis先判断是否有库存然后判断是否下过单如果条件都满足则直接通知用户下单成功。向订单信息保存到消息队列中由独立进程慢慢的将所有的订单信息都写入数据库。

在这里插入图片描述

消息队列的好处在于解耦。最简单的例子生活中取快递的例子。消息队列就相当于菜鸟驿站。快递员就相当于生产者快递员(生产者)把快递放到快递柜里边(Message Queue)去我们(消费者)从快递柜里边去拿东西这就是一个异步两者之间没有耦合。但是如果去掉菜鸟驿站让快递员亲手交给我那么如果我不在家那么快递员就只能等待这就浪费了大量的时间耦合性高。

5. 好友点赞和关注

5.1 好友点赞

好友点赞

好友点赞的问题应该保证一个好友一个笔记只能点赞一次。因此考虑使用Redis中的set集合实现这个功能。

具体实现方法

在Redis中建立set集合每一个日志对应一个set集合set集合中保存的数据就是用户点赞的用户id这样在用户点赞的时候就可以先查询一下是否点过赞如果点过赞则返回错误信息否则则将用户id记录到set集合中。

有些情况下还会显示最近点赞的用户我们可以修改set集合为zsetvalue值就是点赞的时间戳这样倒序排列求前五个用户即可

5.2 好友共同关注

好友共同关注可以利用Redis中的集合求交集运算来实现。在Redis中保存set集合每一个集合对应一个用户集合里面的内容就是该用户关注用户的用户id这样两个用户求共同关注只需要使用交集运算即可实现。

6. Feed流的实现

6.1 Feed流的实现方式

实现Feed流的三种方式

  • 拉模式也叫做读扩散。当张三和李四和王五发了消息后都会保存在自己的邮箱中假设赵六要读取信息那么他会从读取他自己的收件箱此时系统会从他关注的人群中把他关注人的信息全部都进行拉取然后在进行排序

优点比较节约空间因为赵六在读信息时并没有重复读取而且读取完之后可以把他的收件箱进行清楚。
缺点比较延迟当用户读取数据时才去关注的人里边去读取数据假设用户关注了大量的用户那么此时就会拉取海量的内容对服务器压力巨大。

  • 推模式也叫做写扩散。推模式是没有写邮箱的当张三写了一个内容此时会主动的把张三写的内容发送到他的粉丝收件箱中去假设此时李四再来读取就不用再去临时拉取了。

优点时效快不用临时拉取
缺点内存压力大假设一个大V写信息很多人关注他 就会写很多分数据到粉丝那边去

  • 推拉结合模式也叫做读写混合兼具推和拉两种模式的优点。推拉模式是一个折中的方案。
    • 站在发件人这一段如果是个普通的人那么我们采用写扩散的方式直接把数据写入到他的粉丝中去因为普通的人他的粉丝关注量比较小所以这样做没有压力
    • 如果是大V那么他是直接将数据先写入到一份到发件箱里边去然后再直接写一份到活跃粉丝收件箱里边去
    • 现在站在收件人这端来看如果是活跃粉丝那么大V和普通的人发的都会直接写入到自己收件箱里边来而如果是普通的粉丝由于他们上线不是很频繁所以等他们上线时再从发件箱里边去拉信息。

6.2 Feed流的实现

在本项目中实现推模式的Feed流功能。

当用户发送笔记的时候会获取到用户所有关注人的列表然后将用户发送的笔记投放到其粉丝的邮箱中。

当用户刷动态的时候会从自己的邮箱中读取到笔记然后显示。

6.3 Feed流中的难点滚动分页

问题描述

传统的分页一般是基于下标来实现的但是在Feed流中因为我们需要根据动态发布实现逆序排列所有值也就表明最新发布的动态实际上在数据的最上面那么一旦发布新动态那么再使用这种传统的分页方式就会重复的读取部分动态。

传统的分页查询一般通过下标即可实现。但是在feed流中使用下标会产生问题。原因是因为feed流中的数据是不断变化的这就会导致消息的下标也是变化的使用传统的分页方式就会造成数据的重读。下面通过一个例子说明
在这里插入图片描述

  • 假设在t1 时刻我们去读取第一页此时page = 1 size = 5 那么我们拿到的就是10~6 这几条记录
  • 假设现在t2时候又发布了一条记录
  • 此时t3 时刻我们来读取第二页读取第二页传入的参数是page=2 size=5 那么此时读取到的第二页实际上是从6 开始然后是6~2 那么我们就读取到了重复的数据所以feed流的分页不能采用原始方案来做。

解决方案使用滚动分页方式

和传统分页不同我们使用滚动分页每一次记录上次访问的所有动态中时间戳最小的数据下一次再从这个数据开始访问pagesize个动态这样就不会受新发布的动态的影响。

此外在极端情况下还有可能出现同一个时间戳下发布了多个动态的情况。

此外我们还需要考虑时间戳相同的情况。虽然概率很小。
举个例子假设现在有5条数据时间戳分别是 5 5 5 5 4 3 3 2 pageSize是2

  • 第一次查询 5 5 lastId = 5 size = 2
  • 第二次查询 4 3 lastId = 4 size = 2

显然发生了漏读因此针对这种情况我们还需要设置一个偏移量它的值就是本次查询中最小的时间戳出现的次数。

  • 第一次查询 5 5 lastId = 5 size = 2 offset = 2 因为查询结果 5 5 里面最小的时间戳就是5 出现了2次
  • 第二次查询从第1个5开始(包含第一个5)往后走offset个位置的下一个位置就是本次分页的起始位置。
    引入了offset以后就解决了滚动分页查询中时间戳相同导致出漏读问题。

通过使用这种方式就可以解决Feed流中传统分页模式失效的问题。

7. 附近商铺

使用Redis中GEO数据类型实现。将所有的商铺信息按照商铺类型在Redis中建立对应的数据然后进行查询即可。

8. 即时通讯模块

互相关注的好友之间会建立好友关系并可以实现即时通讯功能。

8.1 即时通讯实现的步骤

  • 首先所有注册用户都在环信服务器对应一个用户名和密码这个用户名密码和用户信息都保存在本地的数据库中
  • 此外要想实现好友之间的通讯还需要记录用户之间的好友关系这个可以在点击关注的代码中添加判断如果是共同好友则将好友关系注册到环信中的代码
  • 当用户登录到APP后会从数据库中获取到对应的环信用户名和密码然后自动登录到环信服务器
  • 两个手机端都链接到环信服务器后就可以进行实时的聊天了聊天实际上走的是环信的服务器和本地的探花交友服务器之间没有交互信息。

8.2 代码修改

  • 用户注册在注册用户的同时注册环信用户并注册到环信服务器
  • 用户登录同时从数据库中获取到环信用户和密码然后登录到环信服务器
  • 用户点击关注判断是否为共同关注如果是则需要注册好友关系到环信服务器
  • 用户取消关注需要删除环信服务器中的好友关系

真正的即时通讯的相关服务器是借助了环信服务器并不走本地

9. 定时任务

在APP的后台会对每一天每一周和每一个月的相关用户活跃数据等信息进行实时的统计并展示。这些统计运算实际上非常消耗数据库的资源因此如果每一次点击这个统计的页面都需要从数据库中重新计算是非常消耗资源的因此我们的解决方法是将这些统计数据单独的存放到一张数据表中并且使用定时任务在服务器压力相对较小的时候来计算这些数据。

这样的话前台再次访问这些统计数据的时候就可以直接从数据表中获取到计算好的结果从而降低数据库压力的同时提高了程序的响应速度。

实现方式就是使用了Spring Task的定时任务通过编写CORN表达式来控制程序的执行。

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6