专注app软件定制开发Spring Cloud——负载均衡Ribbon

📢📢📢📣📣📣

 

哈喽!大家好,我是【一心同学】,专注app软件定制开发一位上进心十足的【Java领域博主】!😜😜😜

✨【一心同学】的写作风格:喜欢用【通俗易懂】专注app软件定制开发的文笔去讲解每一个知识点,专注app软件定制开发而不喜欢用【高大上】专注app软件定制开发的官方陈述。

✨【一心同学】博客的领域是【专注app软件定制开发面向后端技术】的学习,专注app软件定制开发未来会持续更新更多的【后端技术】以及【学习心得】。

✨如果有对【后端技术】感兴趣的【小可爱】,欢迎关注一心同学】💞💞💞

❤️❤️❤️专注app软件定制开发感谢各位大可爱小可爱!❤️❤️❤️ 


目录


一、专注app软件定制开发什么是负载均衡?

负载均衡(Load Balance),专注app软件定制开发意思是将负载(工作任务,访问请求)进行平衡、专注app软件定制开发分摊到多个操作单元(服务器,组件)专注app软件定制开发上进行执行。是解决高性能,单点故障(高可用),扩展性(水平伸缩)的终极解决方案。

例子:

在早高峰乘地铁时候,紧挨小区的地铁口人特别多,一般会有限流,还会有个地铁工作人员Y用那个大喇叭在喊“着急的人员请走B口,B口人少车空”。。。

而这个地铁工作人员Y就是负责负载均衡的为了提升网站的各方面能力,我们一般会把多台机器组成一个集群对外提供服务。然而,我们的网站对外提供的访问入口都是一个的,比如www.taobao.com。那么当用户在浏览器输入www.taobao.com的时候如何将用户的请求分发到集群中不同的机器上呢,这就是负载均衡在做的事情。

二、负载均衡分类

注:LB,即负载均衡 (LoadBalancer)

2.1  集中式LB

即在服务的提供方消费方之间使用独立的LB设施,如Nginx(反向代理服务器),由该设施负责把访问请求通过某种策略转发至服务的提供方!

2.2  进程式 LB

将LB逻辑集成到消费方消费方从服务注册中心获知有哪些地址可用,然后自己再从这些地址中选出一个合适的服务器。

Ribbon 就属于进程式LB,它只是一个类库,集成于消费方进程,消费方通过它来获取到服务提供方的地址!

三、Spring Cloud 是什么?

(1)Ribbon负责实现客户端的负载均衡,负载均衡器提供很多对http和tcp的行为控制。


(2)Ribbon默认提供了很多负载均衡算法,如:轮询、随机等,也可以实现自定义的负载均衡算法

(3)在Spring cloud中,当Ribbon与结合使用时,Ribbon可以自动的从Eureka Server获取服务列表,基于,进行服务调用。

(4)在Spring Cloud构建的微服务系统中,Ribbon作为客户端负载均衡器,有两种使用方式,第一种是和RestTemplate相结合,第二种是和Feign相结合。

四、Ribbon 的负载均衡算法

(1)RoundRobinRule:轮询策略,默认策略。


(2)RandomRule,随机,使用Random对象从服务列表中随机选择一个服务。


(3)RetryRule,轮询 + 重试。


(4)WeightedResponseTimeRule:优先选择响应时间快,此策略会根据平均响应时间计算所有服务的权重,响应时间越快,服务权重越重、被选中的概率越高。此类有个DynamicServerWeightTask的定时任务,默认情况下每隔30秒会计算一次各个服务实例的权重。刚启动时,如果统计信息不足,则使用RoundRobinRule策略,等统计信息足够,会切换回来。


(5)AvailabilityFilteringRul:,可用性过滤,会先过滤掉以下服务:由于多次访问故障而断路器处于打开的服务、并发的连接数量超过阈值,然后对剩余的服务列表按照RoundRobinRule策略进行访问。


(6)BestAvailableRule:优先选择并发请求最小的,刚启动时吗,如果统计信息不足,则使用RoundRobinRule策略,等统计信息足够,才会切换回来。


(7)ZoneAvoidanceRule:可以实现避免可能访问失效的区域(zone)
 

五、环境准备-搭建Eureka

1、建立Maven父工程

编写pom.xml

  1. <dependencyManagement>
  2. <dependencies>
  3. <dependency>
  4. <groupId>org.springframework.cloud</groupId>
  5. <artifactId>spring-cloud-alibaba-dependencies</artifactId>
  6. <version>0.2.0.RELEASE</version>
  7. <type>pom</type>
  8. <scope>import</scope>
  9. </dependency>
  10. <!--springCloud的依赖-->
  11. <dependency>
  12. <groupId>org.springframework.cloud</groupId>
  13. <artifactId>spring-cloud-dependencies</artifactId>
  14. <version>Hoxton.SR12</version>
  15. <type>pom</type>
  16. <scope>import</scope>
  17. </dependency>
  18. <!--SpringBoot-->
  19. <dependency>
  20. <groupId>org.springframework.boot</groupId>
  21. <artifactId>spring-boot-dependencies</artifactId>
  22. <version>2.3.12.RELEASE</version>
  23. <type>pom</type>
  24. <scope>import</scope>
  25. </dependency>
  26. </dependencies>
  27. </dependencyManagement>

2、建立以下子工程

注:同样也是Maven。

3、配置springcloud-eureka-7001 

(1)目录如下

(2)导入依赖

  1. <dependencies>
  2. <!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-eureka-server -->
  3. <!--导入Eureka Server依赖-->
  4. <dependency>
  5. <groupId>org.springframework.cloud</groupId>
  6. <artifactId>spring-cloud-starter-eureka-server</artifactId>
  7. <version>1.4.6.RELEASE</version>
  8. </dependency>
  9. <!--热部署工具-->
  10. <dependency>
  11. <groupId>org.springframework.boot</groupId>
  12. <artifactId>spring-boot-devtools</artifactId>
  13. </dependency>
  14. </dependencies>


(3)编写配置文件


application.yml:

  1. server:
  2. port: 7001
  3. # Eureka配置
  4. eureka:
  5. instance:
  6. # Eureka服务端的实例名字
  7. hostname: localhost
  8. client:
  9. # 表示是否向 Eureka 注册中心注册自己(这个模块本身是服务器,所以不需要)
  10. register-with-eureka: false
  11. # fetch-registry如果为false,则表示自己为注册中心,客户端的化为 ture
  12. fetch-registry: false
  13. # Eureka监控页面~
  14. service-url:
  15. defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
 

(4)编写启动器

注意:要在主启动器上方添加 @EnableEurekaServer表示 服务端的启动类,可以接受别人注册进来。

  1. package com.yixin.springcloud;
  2. import org.springframework.boot.SpringApplication;
  3. import org.springframework.boot.autoconfigure.SpringBootApplication;
  4. import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
  5. @SpringBootApplication
  6. @EnableEurekaServer
  7. public class EurekaServer_7001 {
  8. public static void main(String[] args) {
  9. SpringApplication.run(EurekaServer_7001.class,args);
  10. }
  11. }

4、配置springcloud-provider-blog-8001

(1)建立目录如下

(2)导入依赖

  1. <!--导包~-->
  2. <dependencies>
  3. <!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-eureka-server -->
  4. <!--导入Eureka Server依赖-->
  5. <dependency>
  6. <groupId>org.springframework.cloud</groupId>
  7. <artifactId>spring-cloud-starter-eureka</artifactId>
  8. <version>1.4.6.RELEASE</version>
  9. </dependency>
  10. <!--Spring Boot-->
  11. <dependency>
  12. <groupId>org.springframework.boot</groupId>
  13. <artifactId>spring-boot-test</artifactId>
  14. <version>2.4.5</version>
  15. </dependency>
  16. <dependency>
  17. <groupId>org.springframework.boot</groupId>
  18. <artifactId>spring-boot-starter-web</artifactId>
  19. <version>2.4.5</version>
  20. </dependency>
  21. <!--热部署工具-->
  22. <dependency>
  23. <groupId>org.springframework.boot</groupId>
  24. <artifactId>spring-boot-devtools</artifactId>
  25. </dependency>
  26. </dependencies>

(3)编写配置文件

application.yml:

  1. server:
  2. port: 8001
  3. spring:
  4. application:
  5. name: springcloud-provider-blog
  6. # Eureka配置:配置服务注册中心地址
  7. eureka:
  8. client:
  9. service-url:
  10. defaultZone: http://localhost:7001/eureka/
  11. instance:
  12. instance-id: springcloud-provider-dept-8001 #修改Eureka上的默认描述信息

(4)编写BlogController

注:开发中,我们是需要连接到数据库的,但为了给大家演示清楚Ribbon,这里我们用简单dbsource来表示我们这个微服务对应的数据库。

  1. package com.yixin.springcloud.controller;
  2. import org.springframework.beans.factory.annotation.Value;
  3. import org.springframework.web.bind.annotation.GetMapping;
  4. import org.springframework.web.bind.annotation.RestController;
  5. @RestController
  6. public class BlogController {
  7. //表示db01这个数据库
  8. @Value("db01")
  9. private String dbsource;
  10. //注册进来的微服务,获取一些消息
  11. @GetMapping("/blog/info")
  12. public String discovery(){
  13. return dbsource;
  14. }
  15. }

(5)编写启动类BlogProvider_8001

  1. package com.yixin.springcloud;
  2. import org.springframework.boot.SpringApplication;
  3. import org.springframework.boot.autoconfigure.SpringBootApplication;
  4. import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
  5. import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
  6. @SpringBootApplication
  7. @EnableEurekaClient
  8. public class BlogProvider_8001 {
  9. public static void main(String[] args) {
  10. SpringApplication.run(BlogProvider_8001.class,args);
  11. }
  12. }

5、编写springcloud-provider-blog-8002和springcloud-provider-blog-8003

(1)导入的依赖和springcloud-provider-blog-8001一样

(2) 配置springcloud-provider-blog-8002

a、application.yml(其实就是改了端口号而已)

  1. server:
  2. port: 8002
  3. spring:
  4. application:
  5. name: springcloud-provider-blog
  6. # Eureka配置:配置服务注册中心地址
  7. eureka:
  8. client:
  9. service-url:
  10. defaultZone: http://localhost:7001/eureka/
  11. instance:
  12. instance-id: springcloud-provider-dept-8002 #修改Eureka上的默认描述信息

b、编写BlogController

更改数据库名:db02

  1. package com.yixin.springcloud.controller;
  2. import org.springframework.beans.factory.annotation.Value;
  3. import org.springframework.web.bind.annotation.GetMapping;
  4. import org.springframework.web.bind.annotation.RestController;
  5. @RestController
  6. public class BlogController {
  7. //表示db02这个数据库
  8. @Value("db02")
  9. private String dbsource;
  10. //注册进来的微服务,获取一些消息
  11. @GetMapping("/blog/info")
  12. public String discovery(){
  13. return dbsource;
  14. }
  15. }

c、编写启动类,和springcloud-provider-blog-8001一样

(3) 配置springcloud-provider-blog-8003

a、application.yml(其实就是改了端口号而已)

  1. server:
  2. port: 8003
  3. spring:
  4. application:
  5. name: springcloud-provider-blog
  6. # Eureka配置:配置服务注册中心地址
  7. eureka:
  8. client:
  9. service-url:
  10. defaultZone: http://localhost:7001/eureka/
  11. instance:
  12. instance-id: springcloud-provider-dept-8003 #修改Eureka上的默认描述信息

b、BlogController

更改数据库名:db03

  1. package com.yixin.springcloud.controller;
  2. import org.springframework.beans.factory.annotation.Value;
  3. import org.springframework.web.bind.annotation.GetMapping;
  4. import org.springframework.web.bind.annotation.RestController;
  5. @RestController
  6. public class BlogController {
  7. //表示db03这个数据库
  8. @Value("db03")
  9. private String dbsource;
  10. //注册进来的微服务,获取一些消息
  11. @GetMapping("/blog/info")
  12. public String discovery(){
  13. return dbsource;
  14. }
  15. }

c、编写启动类,和springcloud-provider-blog-8001一样。

6、配置springcloud-consumer-blog-80

(1)目录如下

(2)导入依赖

  1. <!--导包~-->
  2. <dependencies>
  3. <!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-eureka-server -->
  4. <!--导入Eureka依赖-->
  5. <dependency>
  6. <groupId>org.springframework.cloud</groupId>
  7. <artifactId>spring-cloud-starter-eureka</artifactId>
  8. <version>1.4.6.RELEASE</version>
  9. </dependency>
  10. <!--Spring Boot-->
  11. <dependency>
  12. <groupId>org.springframework.boot</groupId>
  13. <artifactId>spring-boot-test</artifactId>
  14. <version>2.4.5</version>
  15. </dependency>
  16. <dependency>
  17. <groupId>org.springframework.boot</groupId>
  18. <artifactId>spring-boot-starter-web</artifactId>
  19. <version>2.4.5</version>
  20. </dependency>
  21. <!--热部署工具-->
  22. <dependency>
  23. <groupId>org.springframework.boot</groupId>
  24. <artifactId>spring-boot-devtools</artifactId>
  25. </dependency>
  26. </dependencies>

(3)编写ConfigBean

  1. package com.yixin.springcloud.config;
  2. import org.springframework.context.annotation.Bean;
  3. import org.springframework.context.annotation.Configuration;
  4. import org.springframework.web.client.RestTemplate;
  5. @Configuration
  6. public class ConfigBean {
  7. @Bean
  8. public RestTemplate getRestTemplate(){
  9. return new RestTemplate();
  10. }
  11. }

(4)编写BlogConsumerController

  1. package com.yixin.springcloud.controller;
  2. import org.springframework.beans.factory.annotation.Autowired;
  3. import org.springframework.web.bind.annotation.GetMapping;
  4. import org.springframework.web.bind.annotation.RestController;
  5. import org.springframework.web.client.RestTemplate;
  6. @RestController
  7. public class BlogConsumerController {
  8. @Autowired
  9. private RestTemplate restTemplate;
  10. private static final String REST_URL_PREFIX="http://localhost:8001";
  11. @GetMapping("/consumer/blog")
  12. public String get(){
  13. return "消费端:"+restTemplate.getForObject(REST_URL_PREFIX +"/blog/info", String.class);
  14. }
  15. }

(5)测试

依次启动:

springcloud-eureka-7001

springcloud-provider-blog-8002

springcloud-consumer-blog-80

访问:http://localhost:8000/consumer/blog

至此,Eureka就搭建好了!

六、集成Ribbon

6.1 搭建Ribbon

由于Ribbon属于进程式 LB(Load Balance),即将LB逻辑集成到消费方消费方从服务注册中心获知有哪些地址可用,所以我们只需要在消费方这边进行配置即可。

Eureka搭建好了,我们集成Ribbon非常简单,只需三步

配置springcloud-consumer-blog-80

(1)添加依赖:

  1. <!--Ribbon-->
  2. <dependency>
  3. <groupId>org.springframework.cloud</groupId>
  4. <artifactId>spring-cloud-starter-ribbon</artifactId>
  5. <version>1.4.6.RELEASE</version>
  6. </dependency>


(2)在配置类增加注解@LoadBalanced
 

  1. package com.yixin.springcloud.config;
  2. import org.springframework.cloud.client.loadbalancer.LoadBalanced;
  3. import org.springframework.context.annotation.Bean;
  4. import org.springframework.context.annotation.Configuration;
  5. import org.springframework.web.client.RestTemplate;
  6. @Configuration
  7. public class ConfigBean {
  8. @LoadBalanced //配置负载均衡实现RestTemplate
  9. @Bean
  10. public RestTemplate getRestTemplate(){
  11. return new RestTemplate();
  12. }
  13. }


(3)修改BlogConsumerController获取路径
 

  1. package com.yixin.springcloud.controller;
  2. import org.springframework.beans.factory.annotation.Autowired;
  3. import org.springframework.web.bind.annotation.GetMapping;
  4. import org.springframework.web.bind.annotation.RestController;
  5. import org.springframework.web.client.RestTemplate;
  6. @RestController
  7. public class BlogConsumerController {
  8. @Autowired
  9. private RestTemplate restTemplate;
  10. //private static final String REST_URL_PREFIX="http://localhost:8001";
  11. private static final String REST_URL_PREFIX="http://SPRINGCLOUD-PROVIDER-BLOG";
  12. @GetMapping("/consumer/blog")
  13. public String get(){
  14. return "消费端:"+restTemplate.getForObject(REST_URL_PREFIX +"/blog/info", String.class);
  15. }
  16. }

注意:这里的SPRINGCLOUD-PROVIDER-BLOG指的就是我们服务注册中的服务名。
所以为了搭建服务产生方集群,我们刚刚在搭建springcloud-provider-blog-8001、springcloud-provider-blog-8002、springcloud-provider-blog-8003的时候,我们就已经将其服务名全部设置为一样的了。


 

至此,Ribbon就搭建好了,Ribbon的默认负载均衡算法轮询算法,也就是说,请求结束后都会向下一个服务端发送请求,例如 我们的有服务生产方A8001,服务端生产方B8002,服务端生产方C8003,那么消费端请求三次,依次的顺序是A,B,C。

我们来进行测试下:

依次启动

springcloud-eureka-7001

springcloud-provider-blog-8001

springcloud-provider-blog-8002

springcloud-provider-blog-8003

springcloud-consumer-blog-80

访问:http://localhost:7001/

可以发现,我们三个服务生产方已经成功绑定了。

接着重头戏来了!

进行测试:

访问:http://localhost:8000/consumer/blog

访问第一次:

访问第二次:

访问第三次:

成功啦!!!

6.2  切换负载均衡的规则

修改springcloud-consumer-blog-80下的ConfigBean

  1. package com.yixin.springcloud.config;
  2. import com.netflix.loadbalancer.IRule;
  3. import com.netflix.loadbalancer.RandomRule;
  4. import org.springframework.cloud.client.loadbalancer.LoadBalanced;
  5. import org.springframework.context.annotation.Bean;
  6. import org.springframework.context.annotation.Configuration;
  7. import org.springframework.web.client.RestTemplate;
  8. @Configuration
  9. public class ConfigBean {
  10. @LoadBalanced //配置负载均衡实现RestTemplate
  11. @Bean
  12. public RestTemplate getRestTemplate() {
  13. return new RestTemplate();
  14. }
  15. /**
  16. * IRule:
  17. * RoundRobinRule 轮询策略
  18. * RandomRule 随机策略
  19. * AvailabilityFilteringRule : 会先过滤掉,跳闸,访问故障的服务~,对剩下的进行轮询~
  20. * RetryRule : 会先按照轮询获取服务~,如果服务获取失败,则会在指定的时间内进行,重试
  21. */
  22. @Bean
  23. public IRule myRule() {
  24. return new RandomRule();//使用随机策略
  25. //return new RoundRobinRule();//使用轮询策略
  26. //return new AvailabilityFilteringRule();//会先过滤掉,跳闸,访问故障的服务~,对剩下的进行轮询~
  27. //return new RetryRule();//会先按照轮询获取服务~,如果服务获取失败,则会在指定的时间内进行,重试
  28. }
  29. }

测试:

重启springcloud-consumer-blog-80

访问:http://localhost:8000/consumer/blog

访问第一次:

访问第二次:

访问第三次:

可以发现它是随机的!

6.3 自定义负载均衡的规则

在myRule包下自定义一个配置类MyRule.java。

注意:myRule包不要和主启动类所在的包同级。

(1)编写自定义规则MyRule

规则:每个服务访问5次,换下一个服务。

  1. package com.yixin.myRule;
  2. import com.netflix.client.config.IClientConfig;
  3. import com.netflix.loadbalancer.AbstractLoadBalancerRule;
  4. import com.netflix.loadbalancer.ILoadBalancer;
  5. import com.netflix.loadbalancer.Server;
  6. import org.springframework.context.annotation.Configuration;
  7. import java.util.List;
  8. import java.util.concurrent.ThreadLocalRandom;
  9. @Configuration
  10. public class MyRule extends AbstractLoadBalancerRule {
  11. // 每个服务访问 5 次,换下一个服务
  12. // total=0 => 默认 0,如果等于 5 ,指向下一个服务节点
  13. // index=0 => 默认 0,如果 total 等于 5 ,index+1
  14. private int total = 0; //被调用的次数
  15. private int currentIndex = 0; //当前谁在提供服务
  16. public Server choose(ILoadBalancer lb, Object key) {
  17. if (lb == null) {
  18. return null;
  19. }
  20. Server server = null;
  21. while (server == null) {
  22. if (Thread.interrupted()) {
  23. return null;
  24. }
  25. List<Server> upList = lb.getReachableServers(); //获得存活的服务
  26. List<Server> allList = lb.getAllServers();//获得全部服务
  27. int serverCount = allList.size();
  28. if (serverCount == 0) {
  29. /*
  30. * No servers. End regardless of pass, because subsequent passes
  31. * only get more restrictive.
  32. */
  33. return null;
  34. }
  35. // int index = chooseRandomInt(serverCount);//在区间内随机获得一个地址
  36. // server = upList.get(index);//从存活的列表中获得
  37. //=================================
  38. total++;
  39. if (total > 5) {
  40. total = 0;
  41. currentIndex++;
  42. }
  43. if (currentIndex >= upList.size()) currentIndex = 0;
  44. server = upList.get(currentIndex);
  45. //=================================
  46. if (server == null) {
  47. /*
  48. * The only time this should happen is if the server list were
  49. * somehow trimmed. This is a transient condition. Retry after
  50. * yielding.
  51. */
  52. Thread.yield();
  53. continue;
  54. }
  55. if (server.isAlive()) {
  56. return (server);
  57. }
  58. // Shouldn't actually happen.. but must be transient or a bug.
  59. server = null;
  60. Thread.yield();
  61. }
  62. return server;
  63. }
  64. protected int chooseRandomInt(int serverCount) {
  65. return ThreadLocalRandom.current().nextInt(serverCount);
  66. }
  67. @Override
  68. public Server choose(Object key) {
  69. return choose(getLoadBalancer(), key);
  70. }
  71. @Override
  72. public void initWithNiwsConfig(IClientConfig clientConfig) {
  73. // TODO Auto-generated method stub
  74. }
  75. }

(2)编写配置类MyRuleConf

作用:将我们写的这个规则注入到Spring中。

  1. package com.yixin.myRule;
  2. import com.netflix.loadbalancer.IRule;
  3. import com.netflix.loadbalancer.RandomRule;
  4. import org.springframework.context.annotation.Bean;
  5. import org.springframework.context.annotation.Configuration;
  6. @Configuration
  7. public class MyRuleConf {
  8. @Bean
  9. public IRule myRule(){
  10. return new MyRule();//自定义规则
  11. }
  12. }

(3)启动类增加注解@RibbonClient

  1. package com.yixin.springcloud;
  2. import com.yixin.myRule.MyRuleConf;
  3. import org.springframework.boot.SpringApplication;
  4. import org.springframework.boot.autoconfigure.SpringBootApplication;
  5. import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
  6. import org.springframework.cloud.netflix.ribbon.RibbonClient;
  7. @SpringBootApplication
  8. @EnableEurekaClient
  9. //在微服务启动的时候就能加载自定义的Ribbon类(自定义的规则会覆盖原有默认的规则)
  10. @RibbonClient(name = "SPRINGCLOUD-PROVIDER-BLOG",configuration = MyRuleConf.class)//开启负载均衡,并指定自定义的规则
  11. public class BlogConsumer_80 {
  12. public static void main(String[] args) {
  13. SpringApplication.run(BlogConsumer_80.class,args);
  14. }
  15. }

(4)测试

 重启 springcloud-consumer-blog-80

访问:http://localhost:8000/consumer/blog

访问1-5次:

访问第6-10次:

访问第11-15次:

自定义规则测试成功!!!


小结

以上就是【一心同学】对基于Spring Cloud的负载均衡Ribbon知识点和实操的讲解,实现负载均衡可以将我们的压力分摊到多个操作单元,大家可以重新回头去看上面【一心同学】对Ribbon的概念讲解,相信对Ribbon的理解马上就悟了!

如果这篇【文章】有帮助到你,希望可以给【一心同学】点个赞👍,创作不易,相比官方的陈述,我更喜欢用【通俗易懂】的文笔去讲解每一个知识点,如果有对【后端技术】感兴趣的小可爱,也欢迎关注❤️❤️❤️ 【一心同学】❤️❤️❤️,我将会给你带来巨大的【收获与惊喜】💕💕!

网站建设定制开发 软件系统开发定制 定制软件开发 软件开发定制 定制app开发 app开发定制 app开发定制公司 电商商城定制开发 定制小程序开发 定制开发小程序 客户管理系统开发定制 定制网站 定制开发 crm开发定制 开发公司 小程序开发定制 定制软件 收款定制开发 企业网站定制开发 定制化开发 android系统定制开发 定制小程序开发费用 定制设计 专注app软件定制开发 软件开发定制定制 知名网站建设定制 软件定制开发供应商 应用系统定制开发 软件系统定制开发 企业管理系统定制开发 系统定制开发