本地锁:本地数据库在提交事务之前需要获取到本地锁和全局锁才能够提交
全局锁:在进行分布式事务时,全局事务的提交需要获取到全局锁,然后在各个本地数据库在通过获取各自数据库的本地锁实现事务提交
AT模式分为两个阶段
维护全局和分支事务的状态,驱动全局事务提交或回滚。
定义全局事务的范围:开始全局事务、提交或回滚全局事务。
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
本次使用的Seata版本选用1.5.1,在该版本中没有了相应的数据库执行文件、file.conf、registry.conf等文件
seata1.5.1下载地址
其他版本下载地址,进入之后选择相应的版本然后点击Downloads即可,会跳转到对应的下载界面,看看是需要下载zip版本还是tar版本
修改confi目录下的application.yaml文件,其余配置按照原先的配置即可,需要修改的地方主要是seata处
server:
port: 7091
spring:
application:
name: seata-server
logging:
config: classpath:logback-spring.xml
file:
path: ${user.home}/logs/seata
extend:
logstash-appender:
destination: 127.0.0.1:4560
kafka-appender:
bootstrap-servers: 127.0.0.1:9092
topic: logback_to_logstash
console:
user:
username: seata
password: seata
seata:
config:
type: nacos #使用nacos作为配置中心
nacos:
server-addr: 192.168.72.130:8848 #启动可能会出现Client not connected,current status:STARTING,那就将地址换成http://192.168.72.130:8848
namespace: public #命名空间配置
group: DEFAULT_GROUP #分组信息配置
username: nacos #如果开启了nacos的验证功能则需要配置
password: nacos
data-id: seataServer.properties #nacos中的配置文件名称
registry:
type: nacos #使用nacos作为注册中心
nacos:
application: seata-server
server-addr: 192.168.72.130:8848 #出现Client not connected,current status:STARTING就配上http
group: DEFAULT_GROUP
namespace: public
cluster: seatatest #此处注意,这的值要和nacos中的配置文件service.vgroupMapping.my_test_tx_group的值一样
username: nacos
password: nacos
##if use MSE Nacos with auth, mutex with username/password attribute
#access-key: ""
#secret-key: ""
security:
secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017
tokenValidityInMilliseconds: 1800000
ignore:
urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.ico,/console-fe/public/**,/api/v1/auth/login
上一步中将nacos作为了配置中心和注册中心,所以nacos中肯定会有相应的配置文件,参照application.yml配置文件中的相关信息可以知道需要在public命名空间中的默认分组新建一个名为seataServer.proterties配置文件,将下列配置写其中,这也是Seata自带的,不过高级的版本中没有了,这里是从官网拷贝过来的。该文件主要就是配置数据库连接信息,在配置数据库连接时需要注意的是根据MySQL的版本不同,连接驱动也不一样,MySQL8以下的使用store.db.driverClassName=com.mysql.jdbc.Driver,mysql8则使用store.db.driverClassName=com.mysql.cj.jdbc.Driver
#此处需要注意,这里的配置需要在nacos中新建一个配置文件,就叫service.vgroupMapping.my_test_tx_group文件里面值就是seatatest和前面的配置registry.nacos.cluster的值是一样的
service.vgroupMapping.my_test_tx_group=seatatest
#这里的地址需要配置成seata所在服务器的地址
service.default.grouplist=127.0.0.1:8091
service.enableDegrade=false
service.disableGlobalTransaction=false
#此处对于数据存储使用的是数据库存储所以需要配置数据库的连接信息
store.mode=db
store.db.datasource=druid
store.db.dbType=mysql
#数据库驱动如果是mysql8使用这个,否则使用com.mysql.jdbc.Driver
store.db.driverClassName=com.mysql.cj.jdbc.Driver
store.db.url=jdbc:mysql://192.168.72.130:3306/seata?useUnicode=true&rewriteBatchedStatements=true
store.db.user=root
store.db.password=root
store.db.minConn=5
store.db.maxConn=30
#此处有四张表的配置,所以需要在数据库中执行对应的SQL创建表
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.distributedLockTable=distributed_lock
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000
#Transaction rule configuration, only for the server
server.recovery.committingRetryPeriod=1000
server.recovery.asynCommittingRetryPeriod=1000
server.recovery.rollbackingRetryPeriod=1000
server.recovery.timeoutRetryPeriod=1000
server.maxCommitRetryTimeout=-1
server.maxRollbackRetryTimeout=-1
server.rollbackRetryTimeoutUnlockEnable=false
server.distributedLockExpireTime=10000
server.xaerNotaRetryTimeout=60000
server.session.branchAsyncQueueSize=5000
server.session.enableBranchAsyncRemove=false
#Transaction rule configuration, only for the client
client.rm.asyncCommitBufferLimit=10000
client.rm.lock.retryInterval=10
client.rm.lock.retryTimes=30
client.rm.lock.retryPolicyBranchRollbackOnConflict=true
client.rm.reportRetryCount=5
client.rm.tableMetaCheckEnable=true
client.rm.tableMetaCheckerInterval=60000
client.rm.sqlParserType=druid
client.rm.reportSuccessEnable=false
client.rm.sagaBranchRegisterEnable=false
client.rm.sagaJsonParser=fastjson
client.rm.tccActionInterceptorOrder=-2147482648
client.tm.commitRetryCount=5
client.tm.rollbackRetryCount=5
client.tm.defaultGlobalTransactionTimeout=60000
client.tm.degradeCheck=false
client.tm.degradeCheckAllowTimes=10
client.tm.degradeCheckPeriod=2000
client.tm.interceptorOrder=-2147482648
client.undo.dataValidation=true
client.undo.logSerialization=jackson
client.undo.onlyCareUpdateColumns=true
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000
client.undo.logTable=undo_log
client.undo.compress.enable=true
client.undo.compress.type=zip
client.undo.compress.threshold=64k
#For TCC transaction mode
tcc.fence.logTableName=tcc_fence_log
tcc.fence.cleanPeriod=1h
#Log rule configuration, for client and server
log.exceptionRate=100
#Metrics configuration, only for the server
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898
transport.type=TCP
transport.server=NIO
transport.heartbeat=true
transport.enableTmClientBatchSendRequest=false
transport.enableRmClientBatchSendRequest=true
transport.enableTcServerBatchSendResponse=false
transport.rpcRmRequestTimeout=30000
transport.rpcTmRequestTimeout=30000
transport.rpcTcRequestTimeout=30000
transport.threadFactory.bossThreadPrefix=NettyBoss
transport.threadFactory.workerThreadPrefix=NettyServerNIOWorker
transport.threadFactory.serverExecutorThreadPrefix=NettyServerBizHandler
transport.threadFactory.shareBossWorker=false
transport.threadFactory.clientSelectorThreadPrefix=NettyClientSelector
transport.threadFactory.clientSelectorThreadSize=1
transport.threadFactory.clientWorkerThreadPrefix=NettyClientWorkerThread
transport.threadFactory.bossThreadSize=1
transport.threadFactory.workerThreadSize=default
transport.shutdown.wait=3
transport.serialization=seata
transport.compressor=none
前面的配置中已经知道需要在数据库中创建表了,但是seata高版本中已经不自带SQL文件了,以下是在旧版本中拷贝过来的模板,在数据库中执行一下SQL文件,其中seata数据库的配置只需要执行一次即可,undo_log文件则需要在每个业务数据库下执行一次
seata库配置
drop database if exists seata;
create database seata character set utf8 collate utf8_bin;
-- -------------------------------- The script used when storeMode is 'db' --------------------------------
-- the table to store GlobalSession data
CREATE TABLE IF NOT EXISTS seata.global_table
(
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`status` TINYINT NOT NULL,
`application_id` VARCHAR(32),
`transaction_service_group` VARCHAR(32),
`transaction_name` VARCHAR(128),
`timeout` INT,
`begin_time` BIGINT,
`application_data` VARCHAR(2000),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`xid`),
KEY `idx_status_gmt_modified` (`status` , `gmt_modified`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
-- the table to store BranchSession data
CREATE TABLE IF NOT EXISTS seata.branch_table
(
`branch_id` BIGINT NOT NULL,
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`resource_group_id` VARCHAR(32),
`resource_id` VARCHAR(256),
`branch_type` VARCHAR(8),
`status` TINYINT,
`client_id` VARCHAR(64),
`application_data` VARCHAR(2000),
`gmt_create` DATETIME(6),
`gmt_modified` DATETIME(6),
PRIMARY KEY (`branch_id`),
KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
-- the table to store lock data
CREATE TABLE IF NOT EXISTS seata.lock_table
(
`row_key` VARCHAR(128) NOT NULL,
`xid` VARCHAR(128),
`transaction_id` BIGINT,
`branch_id` BIGINT NOT NULL,
`resource_id` VARCHAR(256),
`table_name` VARCHAR(32),
`pk` VARCHAR(36),
`status` TINYINT NOT NULL DEFAULT '0' COMMENT '0:locked ,1:rollbacking',
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`row_key`),
KEY `idx_status` (`status`),
KEY `idx_branch_id` (`branch_id`),
KEY `idx_xid_and_branch_id` (`xid` , `branch_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
CREATE TABLE IF NOT EXISTS seata.distributed_lock
(
`lock_key` CHAR(20) NOT NULL,
`lock_value` VARCHAR(20) NOT NULL,
`expire` BIGINT,
primary key (`lock_key`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
INSERT INTO seata.distributed_lock (lock_key, lock_value, expire) VALUES ('AsyncCommitting', ' ', 0);
INSERT INTO seata.distributed_lock (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0);
INSERT INTO seata.distributed_lock (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0);
INSERT INTO seata.distributed_lock (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0);
undo_log配置文件
该SQl的配置文件需要在所有的业务数据库下执行
CREATE TABLE IF NOT EXISTS seata_account.undo_log
(
`branch_id` BIGINT(20) NOT NULL COMMENT 'branch transaction id',
`xid` VARCHAR(100) NOT NULL COMMENT 'global transaction id',
`context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
`rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info',
`log_status` INT(11) NOT NULL COMMENT '0:normal status,1:defense status',
`log_created` DATETIME(6) NOT NULL COMMENT 'create datetime',
`log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime',
UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';
在启动之前需要先启动nacos否则会找不到服务进行注册,启动之后使用命令查看启动日志,日志一般存放在logs目录下的start.out文件中
启动日志中没有报错就是启动成功了,接着访问http://localhost:7091就可以进入登录页了,默认账户名和密码都是seata
最好是换成jdk1.8的环境来启动seata,不然会出现无法创建java虚拟的错误,这种主要有两种方式解决,一种就是换jdk的版本为1.8,还有一种就是修改seata的启动文件,但是我找了没找到相应的修改说明所以不会修改
检查连接信息中配置的数据库用户是否能够进行远程连接
#进入数据库后执行命令查看对应的用户是否支持远程连接
use mysql;
select host from user where user='root';
#如果显示的是localhost则表示不支持远程连接,需要进行修修改
update user set host='%' where user='root';
flush privileges;
检查看看Mysql的版本是8以上及以上还是8以下的,如果是8及以上则需要修改在nacos上的配置文件中的数据库连接驱动为store.db.driverClassName=com.mysql.cj.jdbc.Driver,就是第二步中的配置文件
客户端的配置文件针对于seata.config和seata.registry配置和服务器上seata的配置文件保持一致即可,其中需要说明的就是nacos服务上新增的两个配置文件seataServer.properties和service.vgroupMapping.my_test_tx_group,seataServer.properties是客户端和服务器端共用的配置文件,其中配置的service.vgroupMapping.my_test_tx_group=seatatest属性是事务分组,当配置了这个并且客户端的配置文件中出现了tx-service-group: my_test_tx_group属性,那么就需要在nacos中增加相应的tx-service-group: fsp_my_group配置文件,内容为seatatest,并且确保和seataServer.properties文件在同一命名空间和同一分组下,关系图如下图所示
本案例中主要用于测试使用Seata进行全局事务的管控和步使用Seata时如果发生错误会出现怎样的情况,本案例由三个部分组成,分别是SeataOrder8001、SeataStorage8002、SeataAccount8003,只写一个SeataOrder8001案例剩下的基本类似
执行下面的SQL语句创建对应的业务数据库
/*账户业务数据库*/
drop database if exists seata_account;
create database seata_account character set utf8 collate utf8_bin;
/*创建账户表*/
drop table if exists seata_account.account;
create table seata_account.account
(
id int primary key auto_increment,
username varchar(20) comment '用户名',
used_money double comment '消费金额',
remaining_money double comment '剩余金额'
);
CREATE TABLE IF NOT EXISTS seata_account.undo_log
(
`branch_id` BIGINT(20) NOT NULL COMMENT 'branch transaction id',
`xid` VARCHAR(100) NOT NULL COMMENT 'global transaction id',
`context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
`rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info',
`log_status` INT(11) NOT NULL COMMENT '0:normal status,1:defense status',
`log_created` DATETIME(6) NOT NULL COMMENT 'create datetime',
`log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime',
UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';
/*插入数据*/
insert into seata_account.account(id, username, used_money, remaining_money)
values (1, '张三', 0, 100);
/*订单业务数据库*/
drop database if exists seata_order;
create database seata_order character set utf8 collate utf8_bin;
/*创建订单表*/
drop table if exists seata_order.user_order;
create table seata_order.order
(
id int primary key auto_increment,
username varchar(20) comment '订单消费用户名',
commodity_name varchar(20) comment '购买的商品名称',
commodity_count int comment '购买的商品数量',
price double comment '购买的商品价格',
state int default 0 comment '订单状态'
);
CREATE TABLE IF NOT EXISTS seata_order.undo_log
(
`branch_id` BIGINT(20) NOT NULL COMMENT 'branch transaction id',
`xid` VARCHAR(100) NOT NULL COMMENT 'global transaction id',
`context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
`rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info',
`log_status` INT(11) NOT NULL COMMENT '0:normal status,1:defense status',
`log_created` DATETIME(6) NOT NULL COMMENT 'create datetime',
`log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime',
UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';
/*库存业务数据库*/
drop database if exists seata_storage;
create database seata_storage character set utf8 collate utf8_bin;
/*创建库存表*/
drop table if exists seata_storage.storage;
create table seata_storage.storage
(
id int primary key auto_increment,
commodity_name varchar(20) comment '商品名称',
leftover int comment '剩余数量'
);
CREATE TABLE IF NOT EXISTS seata_storage.undo_log
(
`branch_id` BIGINT(20) NOT NULL COMMENT 'branch transaction id',
`xid` VARCHAR(100) NOT NULL COMMENT 'global transaction id',
`context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
`rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info',
`log_status` INT(11) NOT NULL COMMENT '0:normal status,1:defense status',
`log_created` DATETIME(6) NOT NULL COMMENT 'create datetime',
`log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime',
UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';
insert into seata_storage.storage(id, commodity_name, leftover)
values (1, '牙刷', 10);
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-configartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-seataartifactId>
<exclusions>
<exclusion>
<groupId>io.seatagroupId>
<artifactId>seata-spring-boot-starterartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>io.seatagroupId>
<artifactId>seata-spring-boot-starterartifactId>
<version>1.5.1version>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>2.2.2version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druid-spring-boot-starterartifactId>
<version>1.2.8version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
<dependency>
<groupId>org.examplegroupId>
<artifactId>command-apiartifactId>
<version>1.0-SNAPSHOTversion>
dependency>
dependencies>
server:
port: 8001
spring:
application:
name: seata-order
cloud:
nacos:
discovery:
server-addr: http://192.168.72.130:8848
username: nacos
password: nacos
namespace: 2ee2fad4-97d6-4810-9b7e-97878fa85241 #1
group: SEATA_GROUP
config:
server-addr: http://192.168.72.130
username: nacos
password: nacos
namespace: 2ee2fad4-97d6-4810-9b7e-97878fa85241
group: SEATA_GROUP
datasource:
druid:
url: jdbc:mysql://192.168.72.130:3306/seata_order?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
mybatis:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
seata:
enabled: true
config:
type: nacos
nacos:
data-id: seataServer.properties
group: SEATA_GROUP
namespace: 2ee2fad4-97d6-4810-9b7e-97878fa85241
username: nacos
password: nacos
server-addr: http://192.168.72.130:8848
registry:
type: nacos
nacos:
group: SEATA_GROUP
namespace: 2ee2fad4-97d6-4810-9b7e-97878fa85241
username: nacos
password: nacos
server-addr: http://192.168.72.130:8848
application: seata-server
cluster: seatatest
tx-service-group: fsp_my_group #2
1:使用命名空间的DataId不要直接使用命名空间的名称,不然可能找不到
2:2号标记处的说明看总结
@SpringBootApplication
@EnableDiscoveryClient
@EnableAutoDataSourceProxy //开启数据源代理由seata进行代理
public class SeataStorage8003 {
public static void main(String[] args){
SpringApplication.run(SeataStorage8003.class,args);
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Accessors(chain = true)
public class Order implements Serializable {
/**
*
*/
private Integer id;
/**
* 订单消费用户名
*/
private String username;
/**
* 购买的商品名称
*/
private String commodityName;
/**
* 购买的商品数量
*/
private Integer commodityCount;
/**
* 购买的商品价格
*/
private Double price;
/**
* 订单状态
*/
private Integer state;
private static final long serialVersionUID = 1L;
}
@Mapper
public interface OrderMapper{
@Insert("insert into seata_order.order(id, username, commodity_name, commodity_count, price, state) value(#{id},#{username},#{commodityName},#{commodityCount},#{price},#{state})")
@Options(useGeneratedKeys = true,keyProperty = "id",keyColumn = "id")
int addOrder(Order order);
@Update("update seata_order.order set state=1 where id=#{orderId}")
int updateStateById(@Param("orderId")int id);
}
public interface OrderService{
int addOrder(Order order);
int updateStateById(int id);
}
@Service
public class OrderServiceImpl implements OrderService {
@Resource
private OrderMapper orderMapper;
@Override
public int addOrder(Order order) {
int count=orderMapper.addOrder(order);
return count>0?order.getId():0;
}
@Override
public int updateStateById(int id) {
return orderMapper.updateStateById(id);
}
}
使用feign调用Account服务和Storage服务,所以需要编写相应的Feign
@Component
@FeignClient(value = "seata-account")
public interface AccountFeignService {
@PostMapping("/seataAccount/consumption")
Result<String> consumption(@RequestParam("username")String username,@RequestParam("price")double price);
}
@Component
@FeignClient(value = "seata-storage")
public interface StorageFeignService {
@PostMapping("/seataStorage/consumption")
Result<String> consumption(@RequestParam("commodityName")String commodityName,@RequestParam("count")int count);
}
@Service
@Slf4j
public class ConsumptionService {
@Resource
private OrderServiceImpl orderService;
@Resource
private AccountFeignService accountFeignService;
@Resource
private StorageFeignService storageFeignService;
//因为在订单服务中需要插入订单同时需要修改订单的状态所以使用 @Transactional控制全局事务
@Transactional
//使用@GlobalTransactional对全局事务进行处理,当调用其他微服务出现问题或自身出现问题时进行全局事务回滚
@GlobalTransactional
public Result<String> consumption(Order order){
log.info("购物订单信息:{}",order.toString());
int orderId=orderService.addOrder(order);
//调用远程账户扣减服务
Result<String> accountResult=accountFeignService.consumption(order.getUsername(),order.getPrice());
//调用远程库存扣减服务
Result<String> storageResult=storageFeignService.consumption(order.getCommodityName(),order.getCommodityCount());
if(accountResult.getCode()==200&&storageResult.getCode()==200){
orderService.updateStateById(orderId);
log.info("金额扣减完成,库存扣减完成,更新订单状态");
return new Result<>();
}
return new Result<>("下单失败");
}
}
@RestController
@RequestMapping("/seataOrder")
public class TestController {
@Resource
private ConsumptionService consumptionServer;
@PostMapping("/consumption")
public Result<String> consumption(@RequestBody Order order){
return consumptionServer.consumption(order);
}
}
整体项目目录结构