seata线上问题汇总
问题一:死锁
记录一次分布式事务seata
遇到的巨坑报错,开发环境没有问题,但是测试环境频繁出现事务报错。
报错提示:
org.springframework.orm.jpa.JpaSystemException: Unable to commit JDBC Connection;
nested exception is org.hibernate.TransactionException:Unable to commit against JDBC Connection
在开发环境从没遇过这样的报错,但是测试环境经常会出现,而且某些数据在编辑时候只要报了一次这样的错,后面编辑都会一直报这个错。
原因:
查看了一下这两个环境不一样的地方是防火墙,测试环境防火墙设置了老化时间为20
分钟,而开发环境设置了防火墙为不老化,由于分布式事务服务与数据库需要一直保持长连接,但防火墙超过20
分钟后会删除该会话,导致部分新增事务控制的数据库锁记录未被删除,因此数据超时异常。
解决办法:
将防火墙设置为不老化,不再出现死锁。
问题二:死锁(二)
上一次已经踩过一个坑了,这次报错没想到又遇到类似的锁问题了,查看全局锁的表lock_table
发现确实又锁了一堆数据。
报错提示:
Caused by: org.hibernate.TransactionException: Unable to commit against JDBC Connection
at org.hibernate.resource.jdbc.internal.AbstractLogicalConnectionImplementor.commit(AbstractLogicalConnectionImplementor.java:87)
at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl$TransactionDriverControlImpl.commit(JdbcResourceLocalTransactionCoordinatorImpl.java:272)
at org.hibernate.engine.transaction.internal.TransactionImpl.commit(TransactionImpl.java:104)
at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:533)
... 77 common frames omitted
Caused by: java.sql.SQLException: io.seata.core.exception.RmTransactionException: Response[ TransactionException[Could not found global transaction xid = 11.11.11.50:7003:2021769421, may be has finished.] ]
at io.seata.rm.datasource.ConnectionProxy.recognizeLockKeyConflictException(ConnectionProxy.java:157)
at io.seata.rm.datasource.ConnectionProxy.processGlobalTransactionCommit(ConnectionProxy.java:218)
at io.seata.rm.datasource.ConnectionProxy.doCommit(ConnectionProxy.java:196)
at io.seata.rm.datasource.ConnectionProxy.lambda$commit$0(ConnectionProxy.java:184)
at io.seata.rm.datasource.ConnectionProxy$LockRetryPolicy.execute(ConnectionProxy.java:289)
at io.seata.rm.datasource.ConnectionProxy.commit(ConnectionProxy.java:183)
at org.hibernate.resource.jdbc.internal.AbstractLogicalConnectionImplementor.commit(AbstractLogicalConnectionImplementor.java:81)
... 80 common frames omitted
Caused by: io.seata.core.exception.RmTransactionException: Response[ TransactionException[Could not found global transaction xid = 11.11.11.50:7003:2021769421, may be has finished.] ]
at io.seata.rm.AbstractResourceManager.branchRegister(AbstractResourceManager.java:69)
at io.seata.rm.DefaultResourceManager.branchRegister(DefaultResourceManager.java:96)
at io.seata.rm.datasource.ConnectionProxy.register(ConnectionProxy.java:238)
at io.seata.rm.datasource.ConnectionProxy.processGlobalTransactionCommit(C
原因:
这次报错看到了后面的一句话may be has finished翻译过来就是可能事务已经结束了,检查了一下代码发现代码中会有一个地方调用其它服务进行批量update
操作,而这个地方耗时比较长,大概知道原因了,这可能是服务重试,或者网络延迟导致调用参与方服务的时候超时,事务已经回滚结束了,这个延迟的请求才到底对应服务,并尝试加入全局事务,但是全局事务已经结束了,所以抛出异常来回滚所做的操作保证一致性。
解决办法:
在配置文件中设置http
建立socket
超时时间和http
读取响应socket
超时时间
# Ribbon配置
ribbon:
# http建立socket超时时间,毫秒
ConnectTimeout: 6000
# http读取响应socket超时时间
ReadTimeout: 12000
问题三:批量保存
项目使用了微服务,并且将一些模块进行了拆分,现在遇到了一个批量保存的场景,而且还是跨服务调用,因此选用了seata
的AT
模式比较简单方便。
seata官网:http://seata.io/zh-cn/docs/overview/what-is-seata.html
在进行一个单据保存时,由于整单保存需要批量保存一千条数据,耗时竟然要8
秒,首先看了下JPA
的saveAll
方法底层其实调用了for
循环一条一条保存,但修改后使用了自定义的批量保存其实还是没得到多大改善。
后来检查发现这个业务加了@GlobalTransactional
注解需要跨服务,虽然这段批量保存不是其他服务的,但也会非常耗时,去掉这个注解之后保存,只需要1秒。
原因:
看了下seata
官网的AT
模式,我个人理解是虽然修改成了批量保存,但是AT
模式是基于本地ACID
事务的关系型数据库的,这些数据插入到数据库时每一条数据都加上了锁,而加锁是很耗时的,当前业务需要批量插入1000
条数据也就是说这1000
条数据每条都加了锁。
问题四:高并发
seata
在高并发下出现的问题:AT模式获取全局锁失败
seata
服务器中出现的异常:
io.seata.core.exception.BranchTransactionException: Global lock acquire failed xid = 127.0.0.1:8091:121648404042354689 branchId = 121648409478172673
后台出现的异常:
Could not commit JDBC transaction; nested exception is io.seata.rm.datasource.exec.LockConflictException: get global lock fail, xid:127.0.0.1:8091:121648404042354689, lockKeys:bs_product_price_goods_item:26
AT模式出现的异常:
org.springframework.transaction.CannotCreateTransactionException: Could not open JDBC Connection for transaction; nested exception is java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 30077ms.
Caused by: java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 30077ms.
at com.zaxxer.hikari.pool.HikariPool.createTimeoutException(HikariPool.java:695)
at com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:197)
at com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:162)
at com.zaxxer.hikari.HikariDataSource.getConnection(HikariDataSource.java:128)
at org.springframework.jdbc.datasource.DataSourceTransactionManager.doBegin(DataSourceTransactionManager.java:261)
... 65 more
问题分析:
事务在提交之前需要获取seata
全局锁,由于全局锁被其他事务占用着,从而出现获取全局锁失败异常;可细看seata
官网描述:https://seata.io/zh-cn/docs/dev/mode/at-mode.html
seata结论:
seata的AT、XA模式都是基于全局事务实现的,在高并发的场景下会出现获取全局锁异常,因此这两种模式都不适用高并发场景;
seata TCC模式性能比AT模式的好一点,但是并发量大于100的话还是不适合;
如果基本没有什么并发量的话,可以选择AT模式;并发量在一百内的话可以使用TCC模式
高并发场景,不适合使用seata,适合用中间件,例如用rocketmq替代seata,可以弥补seata的不足
问题五:高并发超时
模拟场景
@GlobalTransactional
public void purchase(String userId, String commodityCode, int orderCount) {
try {
lock.lock();
Product product = productService.getById(commodityCode);
if (product.getStock() - orderCount > 0) {
LocalDateTime now = LocalDateTime.now();
Account account = accountService.getByUserId(userId);
Orders orders = new Orders();
orders.setCreateTime(now);
orders.setProductId(product.getId());
orders.setReplaceTime(now);
orders.setSum(orderCount );
orders.setAmount(product.getPrice());
orders.setAccountId(account.getId());
product.setStock(product.getStock() - orderCount );
account.setSum(account.getSum() != null ? account.getSum() + orderCount : orderCount);
account.setLastUpdateTime(now);
productService.updateById(product);
accountService.updateById(account);
ordersService.save(orders);
}
} catch (Exception e) {
// TODO: handle exception
throw new RuntimeException();
} finally {
lock.unlock();
}
}
压测后发现,容易出现事务超时的异常,如图:
可以看到出现异常后回滚超时等情况了。
分析原因:
我们可以按照以上的代码看到,官网的介绍比较多的都是注解形势,api提及比较少,我们首先分析刚才代码
服务调用->发现注解->创建事务->等待锁->获取锁->业务处理
可以发现优先创建了事务,这时候如果再大并发下,一直等待抢不到锁的话......这时候会引发上图所示的各种超时异常。
解决方案"
再来看下流程:服务调用->发现注解->创建事务->等待锁->获取锁->业务处理。发现问题所在了不?只要我们能把等待锁获取锁的操作放在创建事务前,这个问题迎刃而解。
用户请求->等待锁->获取锁->服务调用->发现注解->创建事务->业务处理
@GetMapping(value = "purchase")
public Object purchase() throws TransactionException {
try {
lock.lock();
return demoService.purchase(1, 2, 3);
} finally {
// TODO: handle finally clause
lock.unlock();
}
}
比如上述代码这样,再调用前直接更改掉,这样服务内的锁也可以去除,把锁换到了接口来。如果有人说,因为是分布式锁不是本地锁,或者我加锁的地方就是要在service
内咋办?没关系,seata
还有提供一套事务的api
创建方式。
public void purchase(String userId, String commodityCode, int orderCount) {
try {
lock.lock();
Product product = productService.getById(commodityCode);
if (product.getStock() - orderCount > 0) {
GlobalTransaction tx = GlobalTransactionContext.getCurrentOrCreate();
tx.begin(300000, "test-group");
try {
LocalDateTime now = LocalDateTime.now();
Account account = accountService.getByUserId(userId);
Orders orders = new Orders();
orders.setCreateTime(now);
orders.setProductId(product.getId());
orders.setReplaceTime(now);
orders.setSum(orderCount);
orders.setAmount(product.getPrice());
orders.setAccountId(account.getId());
product.setStock(product.getStock() - orderCount );
account.setSum(account.getSum() != null ? account.getSum() + orderCount : orderCount);
account.setLastUpdateTime(now);
productService.updateById(product);
accountService.updateById(account);
ordersService.save(orders);
tx.commit();
} catch (Exception e) {
// TODO: handle exception
tx.rollback();
}
}
} finally {
lock.unlock();
}
}
改为上述代码,使用api进行提交跟回滚操作即可,这样保证了抢到锁后才进行事务的创建。
问题六:序列化异常问题(已解决)
1.4.x
版本在MySQL8.0
下DATETIME
类型转换错误的问题
异常信息
2021-05-16 16:47:32.924 INFO 22268 --- [h_RMROLE_1_8_16] i.seata.rm.datasource.DataSourceManager : branchRollback failed. branchType:[AT], xid:[172.20.48.1:8091:5485518365424181316], branchId:[5485518365424181322], resourceId:[jdbc:mysql://localhost:3306/seata_order], applicationData:[null]. reason:[Branch session rollback failed and try again later xid = 172.20.48.1:8091:5485518365424181316 branchId = 5485518365424181322 com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `java.time.LocalDateTime` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
at [Source: (byte[])"{"@class":"io.seata.rm.datasource.undo.BranchUndoLog","xid":"172.20.48.1:8091:5485518365424181316","branchId":5485518365424181322,"sqlUndoLogs":["java.util.ArrayList",[{"@class":"io.seata.rm.datasource.undo.SQLUndoLog","sqlType":"INSERT","tableName":"t_order","beforeImage":{"@class":"io.seata.rm.datasource.sql.struct.TableRecords$EmptyTableRecords","tableName":"t_order","rows":["java.util.ArrayList",[]]},"afterImage":{"@class":"io.seata.rm.datasource.sql.struct.TableRecords","tableName":"t_order"[truncated 14288 bytes]; line: 1, column: 1249] (through reference chain: io.seata.rm.datasource.undo.BranchUndoLog["sqlUndoLogs"]->java.util.ArrayList[0]->io.seata.rm.datasource.undo.SQLUndoLog["afterImage"]->io.seata.rm.datasource.sql.struct.TableRecords["rows"]->java.util.ArrayList[0]->io.seata.rm.datasource.sql.struct.Row["fields"]->java.util.ArrayList[4]->io.seata.rm.datasource.sql.struct.Field["value"])]
2021-05-16 16:47:32.924 INFO 22268 --- [h_RMROLE_1_8_16] io.seata.rm.AbstractRMHandler : Branch Rollbacked result: PhaseTwo_RollbackFailed_Retryable
2021-05-16 16:47:33.911 INFO 22268 --- [h_RMROLE_1_9_16] i.s.c.r.p.c.RmBranchRollbackProcessor : rm handle branch rollback process:xid=172.20.48.1:8091:5485518365424181316,branchId=5485518365424181322,branchType=AT,resourceId=jdbc:mysql://localhost:3306/seata_order,applicationData=null
2021-05-16 16:47:33.911 INFO 22268 --- [h_RMROLE_1_9_16] io.seata.rm.AbstractRMHandler : Branch Rollbacking: 172.20.48.1:8091:5485518365424181316 5485518365424181322 jdbc:mysql://localhost:3306/seata_order
2021-05-16 16:47:33.914 ERROR 22268 --- [h_RMROLE_1_9_16] i.s.r.d.u.parser.JacksonUndoLogParser : json decode exception, Cannot construct instance of `java.time.LocalDateTime` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
at [Source: (byte[])"{"@class":"io.seata.rm.datasource.undo.BranchUndoLog","xid":"172.20.48.1:8091:5485518365424181316","branchId":5485518365424181322,"sqlUndoLogs":["java.util.ArrayList",[{"@class":"io.seata.rm.datasource.undo.SQLUndoLog","sqlType":"INSERT","tableName":"t_order","beforeImage":{"@class":"io.seata.rm.datasource.sql.struct.TableRecords$EmptyTableRecords","tableName":"t_order","rows":["java.util.ArrayList",[]]},"afterImage":{"@class":"io.seata.rm.datasource.sql.struct.TableRecords","tableName":"t_order"[truncated 14288 bytes]; line: 1, column: 1249] (through reference chain: io.seata.rm.datasource.undo.BranchUndoLog["sqlUndoLogs"]->java.util.ArrayList[0]->io.seata.rm.datasource.undo.SQLUndoLog["afterImage"]->io.seata.rm.datasource.sql.struct.TableRecords["rows"]->java.util.ArrayList[0]->io.seata.rm.datasource.sql.struct.Row["fields"]->java.util.ArrayList[4]->io.seata.rm.datasource.sql.struct.Field["value"])
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `java.time.LocalDateTime` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
at [Source: (byte[])"{"@class":"io.seata.rm.datasource.undo.BranchUndoLog","xid":"172.20.48.1:8091:5485518365424181316","branchId":5485518365424181322,"sqlUndoLogs":["java.util.ArrayList",[{"@class":"io.seata.rm.datasource.undo.SQLUndoLog","sqlType":"INSERT","tableName":"t_order","beforeImage":{"@class":"io.seata.rm.datasource.sql.struct.TableRecords$EmptyTableRecords","tableName":"t_order","rows":["java.util.ArrayList",[]]},"afterImage":{"@class":"io.seata.rm.datasource.sql.struct.TableRecords","tableName":"t_order"[truncated 14288 bytes]; line: 1, column: 1249] (through reference chain: io.seata.rm.datasource.undo.BranchUndoLog["sqlUndoLogs"]->java.util.ArrayList[0]->io.seata.rm.datasource.undo.SQLUndoLog["afterImage"]->io.seata.rm.datasource.sql.struct.TableRecords["rows"]->java.util.ArrayList[0]->io.seata.rm.datasource.sql.struct.Row["fields"]->java.util.ArrayList[4]->io.seata.rm.datasource.sql.struct.Field["value"])
解决办法
是seata
自身的问题,当下有四种解决办法(任选一种即可):
解决方法来源于:LocalDateTime转换异常,springboot版本:2.4.4 · Issue #3620
- 等
seata
官方修复并更新新版本 - https://github.com/seata/seata/pull/3228/files或者看这个
pr
的做法,通过spi
,自定义你的jackson
序列化器 - 查看你代码的实体类时间属性对应的数据库字段类型如果是
datetime
改成timestamp
- 修改
seata
的换序列化方式配置中心中配置client.undo.logSerialization=kryo
,client
端再引入kryo
的依赖包(ruoyi-cloud
项目放到ruoyi-common-core/pom.xml
下即可)
kryo
依赖如下(版本自行选择)
<dependencies>
<dependency>
<groupId>com.esotericsoftware.kryo</groupId>
<artifactId>kryo</artifactId>
<version>2.24.0</version>
</dependency>
<dependency>
<groupId>com.esotericsoftware</groupId>
<artifactId>kryo</artifactId>
<version>4.0.2</version>
</dependency>
<dependency>
<groupId>de.javakaffee</groupId>
<artifactId>kryo-serializers</artifactId>
<version>0.44</version>
</dependency>
</dependencies>
结论
这个问题的seata
本身存在的问题,https://github.com/seata/seata/issues/3866 已经修复过了,等下个版本(1.5.0
)更新的时候升级一下依赖。有需要的话可以自己先加一下kryo
序列化方式。
问题七:AT模式全局事务回滚(已解决)
说下背景吧,公司最近打算用seata
来保证SpringCloud
微服务间的全局事务(AT模式),使用的是seata-server 1.1.0
版本。注册方式Eureka
,seata-server
在测试环境部署了2台!
异常情况:
测试的时候A服务为发起端A->B->C其中B服务在一次方法调用中多次调用C服务更改同一条记录(数据的创建以及更改状态new->wait_pay->pay->success)。于是在A发起的请求结束时B实际调用了3次C服务,导致C服务里有3条undo_log日志,之后B服务发生了异常,C服务(每次都完成了本地事务)进行数据回滚(全局事务undo_log),此时正常情况应当是按照3条记录 1 2 3创建的后先顺序执行回滚,即 3 2 1 这就能保证数据正常回滚!
然而!在多次测试中发现数据回滚有时并不一定是按照 3 2 1 顺序执行,时常会从2开始这就导致2中的记录值和当前数据库值的实际值 有偏差(2中记录修改后的状态为pay,实际数据库里的状态是3中更改后记录的状态success)这就导致了回滚镜像对比发现数据异常,认为是脏数据产生了!无法正确造成回滚。
找原因:
从结果上看数据无法回滚是由于回滚时候undo_log
执行的顺序异常导致的,以此切入!跟踪seata-server
的代码发现,分支事务回滚时会根据全局事务xid
到branch_table
中按照记录生成的时间(gmt_create
) 正序查询所有分支事件记录放入List
中,之后从List
里倒序取出,挨个执行回滚!
然后惊奇的发现,每次出现上述回滚异常都是因为有两条或多条branch_table
记录的gm_create
是相同的,以致于后续回滚查询分支事务的时候无法保证其先后顺序,而后执行回滚的顺序就一样无法保证,才最终导致上述错误!
解决方案:
问题找到了,既然按时间顺序查找不靠谱 那找个靠谱的值来查询不就行了,其中发现branch_id
其实相对来说是递增的但是只相对于同一个服务而言。
private static final AtomicLong UUID = new AtomicLong(1000);
/**
* Generate uuid long
*
* @return the long
*/
public static long generateUUID() {
long id = UUID.incrementAndGet();
if (id >= getMaxUUID()) {
synchronized (UUID) {
if (UUID.get() >= id) {
id -= UUID_INTERNAL;
UUID.set(id);
}
}
}
return id;
}
实际测试环境部署了有2
个seata-server
服务,所以并不能保证两个服务产生的branch_id
有什么可靠的关联性,最终考虑下比较简单的方法是在branch_table
里增加一个自增的字段,使用该字段代替gmt_create
进行上述分支事务查询的排序依据即可 !
(添加了自增的id,之后更改查询语句如下)
经过更改测试后发现确实解决了上述无法顺序回滚带来的问题!在测试时间如果只是启动一个seata-server
服务一般是不会产生上述问题的,时间基本有先后的明显差别!再者branch_id
也是明显增加的也可以用作查询排序依据,单多台seata-server
的时候就不能保证了!本地重新打包各模块后,在seata-server
的lib
包下替换修改后的新的模块jar
即可!
mvn clean install -DskipTests=true
结论
最新版本1.5.1
已经解决异常控制(幂等、空回滚、悬挂)问题。感兴趣的可以看一下seata设计方案中的异常控制。
问题八:字段长度不够(已解决)
这个问题比较简单
org.springframework.transaction.TransactionSystemException: JDBC commit failed; nested exception is java.sql.SQLException: io.seata.core.exception.RmTransactionException: Response[ TransactionException[branch register request failed. xid=47.100.164.120:8091:27272433966331937, msg=Data truncation: Data too long for column 'row_key' at row 1] ]
at org.springframework.jdbc.datasource.DataSourceTransactionManager.translateException(DataSourceTransactionManager.java:435)
at org.springframework.jdbc.support.JdbcTransactionManager.translateException(JdbcTransactionManager.java:188)
at org.springframework.jdbc.datasource.DataSourceTransactionManager.doCommit(DataSourceTransactionManager.java:336)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:743)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:711)
at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:654)
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:407)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:692)
at com.ht.account.service.impl.HtAccountBalanceServiceImpl$$EnhancerBySpringCGLIB$$62d1ba27.addBalance(<generated>)
at com.ht.account.controller.AccountBalanceController.modifyBalance(AccountBalanceController.java:192)
at com.ht.account.controller.AccountBalanceController$$FastClassBySpringCGLIB$$5aba5833.invoke(<generated>)
Caused by: java.sql.SQLException: io.seata.core.exception.RmTransactionException: Response[ TransactionException[branch register request failed. xid=47.100.164.120:8091:27272433966331937, msg=Data truncation: Data too long for column 'row_key' at row 1] ]
at io.seata.rm.datasource.ConnectionProxy.recognizeLockKeyConflictException(ConnectionProxy.java:161)
at io.seata.rm.datasource.ConnectionProxy.processGlobalTransactionCommit(ConnectionProxy.java:252)
at io.seata.rm.datasource.ConnectionProxy.doCommit(ConnectionProxy.java:230)
at io.seata.rm.datasource.ConnectionProxy.lambda$commit$0(ConnectionProxy.java:188)
at io.seata.rm.datasource.ConnectionProxy$LockRetryPolicy.execute(ConnectionProxy.java:333)
at io.seata.rm.datasource.ConnectionProxy.commit(ConnectionProxy.java:187)
at org.springframework.jdbc.datasource.DataSourceTransactionManager.doCommit(DataSourceTransactionManager.java:333)
... 80 common frames omitted
Caused by: io.seata.core.exception.RmTransactionException: Response[ TransactionException[branch register request failed. xid=47.100.164.120:8091:27272433966331937, msg=Data truncation: Data too long for column 'row_key' at row 1] ]
at io.seata.rm.AbstractResourceManager.branchRegister(AbstractResourceManager.java:69)
at io.seata.rm.DefaultResourceManager.branchRegister(DefaultResourceManager.java:96)
at io.seata.rm.datasource.ConnectionProxy.register(ConnectionProxy.java:272)
at io.seata.rm.datasource.ConnectionProxy.processGlobalTransactionCommit(ConnectionProxy.java:250)
... 85 common frames omitted
解决方法:
是因为seata
库的lock_table
表中的字段长度不够引起的,默认值太小,调整为合适大小即可。