百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程网 > 正文

2阶段提交分布式事务中间件Raincat

yuyutoo 2024-10-16 15:47 12 浏览 0 评论

架构设计

流程图

流程图原理详解

首先用户发起request请求进入consume端(也可以是controller层),controller层进入AOP切面,开启分布式事务

  • 会与txManager通信,创建事务组
  • 发起业务方法调用point.proceed(),这一步是分布式事务调用RPC方法的入口,会一个一个调用RPC方法
  • 主线程调用provder1 提供的RPC方法,同样会进入切面,此时,会启动另一个线程去调用该RPC方法,当前线程wait
调用失败,调用业务方法线程,会唤醒等待的主线程,并把异常信息返回,走的就是one fial rollback这条线,消费方获取异常信息,自动回滚当前事务,分布式事务结束。
调用成功 业务线程,将唤醒主线程,并返回数据。业务线程同txManager 通信 将自己加入到当前事务组中,业务线程进入等待,等待txManager指令,commit or rollback 即 图中的4,5,6
  • 主线程继续调用provider2提供的RPC方法 ,同样会进入AOP切面,当前主线程等待,启动一个业务线程去调用业务方法
调用失败,调用业务方法线程,会唤醒等待的主线程,并把异常信息返回,走的就是two fial rollback这条线,消费方获取异常信息,自动回滚当前事务,并与txManager进行通信(two TransactionGroup fail 这条线),txManager
发起rollback指令(two fail rollback 这条线),通知provider1 rollback
调用成功 业务线程,将唤醒主线程,并返回数据。业务线程同txManager 通信 将自己加入到当前事务组中,业务线程进入等待,等待txManager指令,commit or rollback 即 图中的9,10,11
  • 主线程point.proceed()方法成功执行,会向txManager发出commit请求,txManager接收请求后,会检查各模块的网络通信等状态,符合条件后,发出commit指令,即 14。通知各个模块提交自己的事务。

三阶段提交最大的问题,在于当txManager发出commit指令后,业务方down机,或者网络通信断了。怎么解决事务一致性的问题。我们这么想,当txManager 发出commit指令的时候,其实业务方法都没有任何异常,都是执行成功的,那么数据就是需要提交的。那么我们本地的补偿机制正是为了该场景而考虑,如果down机,或者网络异常。本地服务恢复后,会执行补偿,达到数据的一致性。如果业务方是集群部署的话,会立刻执行补偿,达到数据的一致性。

重要提醒,如果调用服务方是timeout,那么会判断当前事务不成功,这次分布式事务统一不会执行。

快速开始:

Spring-Boot-Starter

导出

  • 首先引入rpc框架所支持的jar包,注意:版本号,根据你拉代码自己打包上传到私服一致
  • Dubbo
<dependency>
 <groupId>com.raincat</groupId>
 <artifactId>raincat-spring-boot-starter-dubbo</artifactId>
 <version>${your version}</version>
 </dependency>
  • SpringCloud
<dependency>
 <groupId>com.raincat</groupId>
 <artifactId>raincat-spring-boot-starter-springcloud</artifactId>
 <version>${your version}</version>
 </dependency>
  • Motan
 <dependency>
 <groupId>com.raincat</groupId>
 <artifactId>raincat-spring-boot-starter-motan</artifactId>
 <version>${your version}</version>
 </dependency>
  • 在你的 application.yml 中新增如下配置:
raincat:
 tx :
 txManagerUrl: http://localhost:8761
 serializer: kroy
 nettySerializer: kroy
 compensation: true
 compensationCacheType : db
 txDbConfig :
 driverClassName : com.mysql.jdbc.Driver
 url : jdbc:mysql://10.4.4.202:3306/tx?useUnicode=true&characterEncoding=utf8
 username : root
 password : 123123
 # repositorySupport : redis
 # txRedisConfig :
 # hostName : 192.168.1.68
 # port : 6379
 # password :
 # repositorySupport : zookeeper
 # tx-zookeeper-config :
 # host : 92.168.1.73:2181
 # sessionTimeOut : 100000
 # rootPath : /tx
 # repositorySupport : mongodb
 # tx-mongo-config :
 # mongoDbUrl : localost:27017
 # mongoDbName : xiaoyu
 # mongoUserName : xiaoyu
 # mongoUserPwd : 123456
 # repositorySupport : file
 # tx-file-config:
 # path : /consume
 # prefix : consume

TxManager配置详解

  • application.properties 主要是配置tmManager的http服务端口,redis信息,netty相关信息 注意:因为现在tmManager 是自己向自己注册,所以http端口(server.port)应该要与eureka的端口一致。

server.port=8761 txManager 的http端口

tx.manager.netty.port=9998 对业务方提供的TCP 端口。

tx.manager.netty.serialize=kryo netty 序列化方式,注意应该要与业务方的序列化方式一致。

  • bootstrap.yml 主要是配置eureka的相关属性,比如renew时间,注册地址等
  • 部署集群配置: 1.修改application.properties中的 server.port 如: 第一份服务为 server.port=8761 tx.manager.netty.port=9998; 第二份服务为:server.port=8762 tx.manager.netty.port=9999; 2. 修改bootstrap.yml中的eureka:client:serviceUrl:defaultZone:http://localhost:8761/eureka/,http://localhost:8762/eureka/ 再依次启动

业务方配置详解

 @TxTransaction 该注解为分布式事务的切面(AOP point),如果业务方的service服务需要参与分布式事务,则需要加上此注解

applicationContext.xml 详解:

 <!-- Aspect 切面配置,是否开启AOP切面-->
 <aop:aspectj-autoproxy expose-proxy="true"/>
 <!--扫描分布式事务的包-->
 <context:component-scan base-package="com.raincat.*"/>
 <!--启动类属性配置-->
 <bean id="txTransactionBootstrap" class="com.raincat.core.bootstrap.TxTransactionBootstrap">
 <property name="txManagerUrl" value="http://192.168.1.66:8761"/>
 <property name="serializer" value="kryo"/>
 <property name="nettySerializer" value="kryo"/>
 <property name="blockingQueueType" value="Linked"/>
 <property name="compensation" value="true"/>
 <property name="compensationCacheType" value="db"/>
 <property name="txDbConfig">
 <bean class="com.raincat.common.config.TxDbConfig">
 <property name="url"
 value="jdbc:mysql://192.168.1.78:3306/order?useUnicode=true&characterEncoding=utf8"/>
 <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
 <property name="password" value="password"/>
 <property name="username" value="xiaoyu"/>
 </bean>
 </property>
 </bean>
TxTransactionBootstrap 详解(具体参见com.raincat.core.config.TxConfig):
 <!--这里配置的TxManager http请求的IP:PORT (如果TxManager有改动,这里要跟着改动)-->
 <property name="txManagerUrl" value="http://192.168.1.66:8761"/>
 <!-- 与txManager通信的序列化方式,spi扩展支持 kroy,hessian protostuff 推荐使用kroy-->
 <property name="nettySerializer" value="kryo"/>
 <!-- 线程池中的队列类型 spi扩展支持 Linked Array SynchronousQueue-->
 <property name="blockingQueueType" value="Linked"/>
 <!--线程池中的拒绝策略 spi扩展支持 Abort Blocking CallerRuns Discarded Rejected-->
 <property name="rejectPolicy" value="Abort"/>
 <!--开启本地补偿(默认开启)-->
 <property name="compensation" value="true"/> 
 <!--本地数据序列化方式 spi扩展支持 java kroy,hessian protostuff 推荐使用kroy-->
 <property name="serializer" value="kryo"/>

本地数据保存配置与详解(spi扩展支持db,redis,zookeeper,mongodb,file),详情配置请参照sample工程:

  1. 本地数据存储为数据库(数据库支持mysql,oracle ,sqlServer),当业务模块为集群时,推荐使用 会自动创建表,表名称为 tx_transaction_模块名称(applicationName),每个模块配置成一样的补偿方式,如果是用db进行存储,请使用同一个库来存储。
 <!--配置补偿类型为db-->
 <property name="compensationCacheType" value="db"/>
 <property name="txDbConfig">
 <bean class="com.raincat.common.config.TxDbConfig">
 <!--数据库url-->
 <property name="url"
 value="jdbc:mysql://192.168.1.78:3306/order?useUnicode=true&characterEncoding=utf8"/>
 <!--数据库驱动名称 --> 
 <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
 <property name="password" value="1234567"/>
 <property name="username" value="xiaoyu"/>
 </bean>
 </property>
  1. 本地数据存储为redis,当业务模块为集群时,推荐使用。(更多配置请参考 com.happylifeplat.transaction.core.config.TxRedisConfig)
 <!--配置补偿类型为reids-->
 <property name="compensationCacheType" value="redis"/>
 <property name="txRedisConfig">
 <bean class="com.raincat.common.config.TxRedisConfig">
 <!--redis host-->
 <property name="hostName" value="192.168.1.78"/>
 <!--redis port-->
 <property name="port" value="6379"/>
 <!--redis 密码 (有密码就配置,无密码则不需要配置)-->
 <property name="password" value=""/> 
 </bean>
 </property>
  1. 本地数据存储为zookeeper,当业务模块为集群时,推荐使用
 <!--配置补偿类型为zookeeper-->
 <property name="compensationCacheType" value="zookeeper"/>
 <property name="txZookeeperConfig">
 <bean class="com.raincat.common.config.TxZookeeperConfig">
 <!--zookeeper host:port-->
 <property name="host" value="192.168.1.66:2181"/>
 <!--zookeeper session过期时间-->
 <property name="sessionTimeOut" value="2000"/>
 <!--zookeeper 根节点路径-->
 <property name="rootPath" value="/tx"/>
 </bean>
 </property>
  1. 本地数据存储为mongodb,当业务模块为单节点时,可以使用。会自动创建集合,集合名称为 tx_transaction_模块名称(applicationName) 这里mongdb连接方式采用3.4.0版本推荐使用的Sha1,不是CR模式,同时mongdb应该开启权限认证,使用者需要注意
 <!--配置补偿类型为mongodb-->
 <property name="compensationCacheType" value="mongodb"/>
 <property name="txMongoConfig">
 <bean class="com.raincat.common.config.TxMongoConfig">
 <!--mongodb url-->
 <property name="mongoDbUrl" value="192.168.1.78:27017"/>
 <!--mongodb 数据库-->
 <property name="mongoDbName" value="happylife"/>
 <!--mongodb 用户名-->
 <property name="mongoUserName" value="xiaoyu"/>
 <!--mongodb 密码-->
 <property name="mongoUserPwd" value="123456"/>
 </bean>
 </property>
  1. 本地数据存储为file,当业务模块为单节点时,可以使用。创建的文件名称TX_ + prefix配置 + 模块名称
 <!--配置补偿类型为file-->
 <property name="compensationCacheType" value="file"/>
 <property name="txFileConfig">
 <bean class="com.raincat.common.config.TxFileConfig">
 <!--指定文件路径(可填可不填,不填时候,默认就是当前项目所在的路径)-->
 <property name="path" value=""/>
 <!--指定文件前缀,生成文件名称-->
 <property name="prefix" value="consume"/>
 </bean>
 </property>

dubbo用户指南

导出

dubbo用户指南(jar包的版本问题可以自己git源代码,然后编译上传到自己公司的私服)

  • 配置并且启动txManager,在aoolication.properties修改你的服务端口,redis配置,具体可以参考配置详解
  • 在你的dubbo服务端添加jar包,并在需要参与分布式事务的方法上添加 @TxTransaction注解
 <dependency>
 <groupId>com.raincat</groupId>
 <artifactId>raincat-dubbo</artifactId>
 <version>${your.version}</version>
 </dependency>
  • 配置applicationContext.xml,如下所示,如有其他问题可以参考配置详解
<!-- Aspect 切面配置,是否开启AOP切面-->
 <aop:aspectj-autoproxy expose-proxy="true"/>
 <!--扫描分布式事务的包-->
 <context:component-scan base-package="com.raincat.*"/>
 <!--启动类属性配置-->
 <bean id="txTransactionBootstrap" class="com.raincat.core.bootstrap.TxTransactionBootstrap">
 <!--这里的url是txMangager的ip+端口-->
 <property name="txManagerUrl" value="http://192.168.1.66:8761"/>
 <property name="serializer" value="kryo"/>
 <property name="nettySerializer" value="kryo"/>
 <property name="blockingQueueType" value="Linked"/>
 <property name="compensation" value="true"/>
 <property name="compensationCacheType" value="db"/>
 <property name="txDbConfig">
 <bean class="com.raincat.common.config.TxDbConfig">
 <property name="url"
 value="jdbc:mysql://192.168.1.78:3306/order?useUnicode=true&characterEncoding=utf8"/>
 <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
 <property name="password" value="password"/>
 <property name="username" value="xiaoyu"/>
 </bean>
 </property>
 </bean>
  • 在dubbo的消费方,也是如上配置,配置好以后,启动项目.

快速体检,运行dubbo-sample( 使用者JDK必须为1.8)

步骤一:

配置txManaager, 修改application.properties中你自己的redis配置

启动TxManagerApplication

注意:如果需要修改服务端口,则应该保证与eureka:client:serviceUrl:defaultZone中的端口一致

步骤二:

  1. 引入依赖包(sample已经引入)
<dependency>
 <groupId>com.raincat</groupId>
 <artifactId>raincat-dubbo</artifactId>
 <version>${you.version}</version>
 </dependency>
  1. 执行 raincat-dubbo-sample 工程 sql文件 dubbo-sample.sql
  2. 在每个工程下的application.yml 中配置您的数据库连接(只需要改ip和端口)
  3. 在工程下的spring-dubbo.xml 中配置您的zookeeper注册中心
  4. 在每个工程下 applicationContext.xml中的TxDbConfig 配置您的补偿数据库连接,提供单独的库来存储。
  5. 在需要做分布式事务的接口上加上注解 @TxTransaction (sample已经加上)
  6. 依次启动order模块,stock 模块 ,consume模块
  7. 访问http://localhost:8888/swagger-ui.html 进行测试体验

springcloud用户指南

导出

springcloud用户指南(jar包的版本问题可以自己git源代码,然后编译上传到自己公司的私服)

  • 配置并且启动txManager,在aoolication.properties修改你的服务端口,redis配置,具体可以参考配置详解
  • 在你的springcloud服务端添加jar包,并在需要参与分布式事务的方法上添加 @TxTransaction注解
<dependency>
 <groupId>com.raincat</groupId>
 <artifactId>raincat-springcloud</artifactId>
 <version>${your.version}</version>
</dependency>
  • 配置applicationContext.xml,如下所示,如有其他问题可以参考配置详解
<!-- Aspect 切面配置,是否开启AOP切面-->
 <aop:aspectj-autoproxy expose-proxy="true"/>
 <!--扫描分布式事务的包-->
 <context:component-scan base-package="com.raincat.*"/>
 <!--启动类属性配置-->
 <bean id="txTransactionBootstrap" class="com.raincat.core.bootstrap.TxTransactionBootstrap">
 <!--这里的url是txMangager的ip+端口-->
 <property name="txManagerUrl" value="http://192.168.1.66:8761"/>
 <property name="serializer" value="kryo"/>
 <property name="nettySerializer" value="kryo"/>
 <property name="blockingQueueType" value="Linked"/>
 <property name="compensation" value="true"/>
 <property name="compensationCacheType" value="db"/>
 <property name="txDbConfig">
 <bean class="com.raincat.common.config.TxDbConfig">
 <property name="url"
 value="jdbc:mysql://192.168.1.78:3306/order?useUnicode=true&characterEncoding=utf8"/>
 <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
 <property name="password" value="password"/>
 <property name="username" value="xiaoyu"/>
 </bean>
 </property>
 </bean>
  • 在springcloud的消费方,也是如上配置,配置好以后,启动项目.

快速体检,运行springcloud-sample( 使用者JDK必须为1.8)

步骤一:

配置txManaager, 修改application.properties中你自己的redis配置 
启动TxManagerApplication

注意:如果需要修改服务端口,则应该保证与eureka:client:serviceUrl:defaultZone中的端口一致

步骤二:

  • 引入依赖包(sample已经引入)
 <dependency>
 <groupId>com.raincat</groupId>
 <artifactId>raincat-springcloud</artifactId>
 <version>${your.version}</version>
 </dependency>
  • 执行 raincat-springcloud-sample 工程 sql文件 springcloud-sample.sql
  • 在每个工程下 application.yml 中配置您的数据库连接(只需要改ip和端口)
  • 在每个工程下 applicationContext.xml中的TxDbConfig 配置您的补偿数据库连接,提供单独的数据库来存储。
  • 在需要做分布式事务的接口上加上注解 @TxTransaction (sample已经加上)
  • 依次启动AliPayApplication,WechatApplication ,PayApplication
  • 访问http://localhost:8881/pay-service/swagger-ui.html 进行体验测试

启动事务管理后台(admin)

导出

raincat-admin 启动教程

启动前提:分布式事务项目已经部署并且运行起来,用户根据自己的RPC框架进行使用 dubbo 用户 springcloud 用户

  • 首先用户使用的JDK必须是1.8+ 本地安装了git ,maven ,执行以下命令
git clone https://github.com/yu199195/Raincat.git
maven clean install
  • 使用你的开发工具打开项目,比如idea Eclipse

步骤一: 配置并且启动transaction-admin

  • 在项目中的application.properties文件中,修改您的服务端口,redis配置等配置:
#admin项目的tomcat端口
server.port=8888
#admin项目的上下文
server.context-path=/admin
server.address=0.0.0.0
#admin项目的应用名称
spring.application.name=tx-transaction-admin
# admin项目激活的类型,支持db,file,mongo,zookeeper,redis 下文会继续讲解
spring.profiles.active=db
# txManager redis 配置
#集群配置
#tx.redis.cluster=true
#tx.redis.cluster.nodes=127.0.0.1:70001;127.0.1:7002
#tx.redis.cluster.redirects=20
#单机配置
tx.redis.cluster=false
tx.redis.hostName=192.168.1.68
#redis主机端口
tx.redis.port=6379
#tx.redis.password=happylifeplat01
# admin管理后台的用户名,用户可以自己更改
tx.admin.userName=admin
# admin管理后台的密码,用户可以自己更改
tx.admin.password=admin
#采用二阶段提交项目的应用名称集合,这个必须要填写
recover.application.list=alipay-service,wechat-service,pay-service
#事务补偿最大重试次数
recover.retry.max=10
#事务补偿的序列方式
recover.serializer.support=kryo
#dbSuport 采用db进行的补偿方式的配置
recover.db.driver=com.mysql.jdbc.Driver
recover.db.url=jdbc:mysql://192.168.1.68:3306/tx?useUnicode=true&characterEncoding=utf8
recover.db.username=xiaoyu
recover.db.password=Wgj@555888
#redis 采用redis进行补偿方式的配置
recover.redis.cluster=false
recover.redis.hostName=192.168.1.68
recover.redis.port=6379
recover.redis.password=
#recover.redis.clusterUrl=127.0.0.1:70001;127.0.1:7002
#mongo 采用mongo 进行补偿方式的配置
recover.mongo.url=192.168.1.68:27017
recover.mongo.dbName=happylife
recover.mongo.userName=xiaoyu
recover.mongo.password=123456
#zookeeper 采用zookeeper进行补偿的方式配置
recover.zookeeper.host=192.168.1.132:2181
recover.zookeeper.sessionTimeOut=200000

配置解释

  • 关于txManager的redis配置:其实就是在txManager项目中的redis配置,在这里需要配置成一样的; 注意,如果你的redis是集群模式请参考集群配置,并注释掉单机redis配置,同理redis单机模式也一样。
  • 关于 recover.application.list配置:这里需要配置每个参与二阶段分布式事务的系统模块的applicationName,多个模块用 "," 分隔,这里必须要配置。
  • 关于 recover.serializer.support 配置,这里是指参与二阶段分布式事务系统中,配置事务补偿信息的序列化方式。
  • 关于 spring.profiles.active 配置 admin项目激活的类型,支持db,file,mongo,zookeeper, 这里是指参与二阶段分布式事务系统中,配置事务补偿信息存储方式,如果您用db存储,那这里就配置成db,同时配置好 recover.db等信息。 其他方式同理。 注意,每个模块请使用相同的序列化方式和存储类型
  • 关于 tx.admin 等配置。 这里就是管理后台登录的用户与密码,用户可以进行自定义更改。

步骤二:修改 index.html

<!--href 修改成你的ip 端口-->
<a id="serverIpAddress" style="display: none" href="http://192.168.1.132:8888/admin">

步骤三: 运行 AdminApplication 中的main方法。

步骤四:在浏览器访问 http://ip:prot/admin ,输入用户名,密码登录。

相关推荐

从零搭建高可用的 MySQL 主从复制架构(基于 Linux 实战指南)

背景在生产环境中,单点MySQL数据库容易成为性能瓶颈或单点故障源。搭建MySQL主从复制架构,可以实现读写分离、高可用,提升系统的整体稳定性与扩展性。...

国外大神成功让Nexus4吃上安卓6.0:基本可正常使用

IT之家讯10月9日消息谷歌已经于10月6日正式开启了Nexus设备Android6.0Marshmallow系统的OTA升级推送。根据之前报道的消息,老一批的Nexus手机如Nexus4/Ne...

急死!CPU被挖矿了,却找不到哪个进程

CPU起飞了最近有朋友在群里反馈,自己服务器的CPU一直处于高占用状态,但用...

甜甜的安卓5.0却让手机ROOT难度大大增加

IT之家(www.ithome.com):甜甜的安卓5.0却让手机ROOT难度大大增加对设备进行ROOT,毫无疑问,这是安卓最美丽的地方之一,不管是对于消费者来说还是开发者。Root意味着掌握更多的权...

Linux基础知识(linux基础知识点及答案)

系统目录结构/bin:命令和应用程序。/boot:这里存放的是启动Linux时使用的一些核心文件,包括一些连接文件以及镜像文件。/dev:dev是Device(设备)的缩写,该目录...

Linux 内核 6.15 发布:内存、网络、文件系统全面升级!

核心增强:性能与安全双飞升!Linux内核6.15的正式版!虽然因一个临门一脚的Bug晚了几小时,但最终还是带着一堆硬核更新闪亮登场!...

AlmaLinux 9.6 发布,新增功能亮点纷呈!

距离上一版本AlmaLinux9.5发布六个月后,基于5.14内核的AlmaLinux正式宣布其企业级Linux发行版的9.x系列第六个更新——AlmaLinux9.6(Sag...

理解Linux下的SELinux(linux seccomp)

理解Linux下的SELinux长久以来,每当遇到授权问题或者新安装的主机,我的第一反应是通过setenforce0命令禁用SELinux,来减少产生的权限问题,但是这并不是一个良好的习惯。这篇文章...

3个简单实用的网址导航网站(简洁的网站导航)

在我们使用电脑上网的时候经常会访问某些常用的网站,每一次都去通过搜索访问就比较浪费时间,添加在浏览器收藏夹不方便在其他电脑使用。找一个好用的网址导航网站就可以帮我们把所有常用的网址集合在一个页面,方便...

整点不一样的网站制作教程,教你怎么用网站模板制作网站#...

网站制作教程整点不一样的网站。不要再问我网站制作教程了,今天给你整个怎么用网站模板制作网站的教程。·1、登录账号进入后台。·2、选择模板。自助建站平台通常提供各种各样的网站模板,可以根据自己的需求和喜...

5个最好的外贸独立站模板,让你的网站更加专业

作为外贸行业从业者,一个专业且具有吸引力的网站是必不可少的。然而,建立一个专业的网站需要耗费大量的时间和精力,尤其是在设计和开发方面。为了帮助您缩短网站建设的时间和成本,以下是5个最好的外贸独立站模板...

网站建设模板 **网站建设模板:全面指南与创意构思*

网站建设模板**网站建设模板:全面指南与创意构思**随着互联网技术的迅猛发展,网站已成为企业、机构和个人展示自身形象、传递信息、实现交流的重要平台。本文将详细介绍网站建设的基本模板,并提供创意...

原地封神!一个只用套模板即可制作电子相册的网站

对于忙碌的年轻人来说,一键操作的模板意味着无需复杂的操作步骤,就能轻松制作出精美的电子相册。但是一个好的工具也是事关重要,最近发现了一款非常适合年轻人的模板---FLBOOK在线制作电子杂志平台,只需...

跨屏建站网kpfree免费网站模板2023.1.14发布更新

跨屏建站网kpfree免费网站模板2023.1.14发布更新,摒弃了之前的卡片式设计,采用了移动优先的设计原则,简化了页面设计风格,优化了代码,优化了图片质量,确保网页打开速度。砍掉了一些花哨而无用的...

响应式大型电子企业集团类网站模板源码-青柠资源网qnziyw.cn

模板信息:模板编号:10964模板编码:UTF8模板颜色:红色模板分类:科技、电子、数码设备适合行业:电子设备类企业模板介绍:本模板自带eyoucms内核,无需再下载eyou系统,原创设计、手工书写D...

取消回复欢迎 发表评论: