spring-boot-learning-事务处理
阅读原文时间:2023年07月08日阅读:1

事务处理的重要性:

面对高井发场景, 掌握数据库事务机制是至关重要的,它能够帮助我们在一定程度上保证数据的一致性,并且有效提高系统性能,避免系统产生岩机,这对于互联网企业应用的成败是至关重要的。

以前的数据库事务处理;

在Spri n g 中,数据库事务是通过AOP 技术来提供服务的。在JDBC 中存在着大量的
try …. catch … fina ll y . . . 语句,也同时存在着大量的元余代码,如那些打开和关闭数据库连接的代码以及
事务回滚的代码。使用Spring AOP 后, Spring 将它们擦除了, 你将看到更为干净的代码,没有那些
tη.. . catch … fina ll y . . . 语句,也没有大量的冗余代码

传播行为的应用场景

一个批处理,它将处理多个交易,但是在一些交易中发生了异常, 这个时候则不能将所有的交易都回

滚。如果所有的交易都回渎,那么那些本能够正常处理的业务也无端地被回滚了,这显然不是我们
所期待的结果。通过Spring 的数据库事务传播行为,可以很方便地处理这样的场景。

统一我们先配置数据库连接信息:

spring.datasource.url=jdbc:mysql://localhost:3306/springboot
spring.datasource.username=root
spring.datasource.password=1997
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
server.port=8181

spring.datasource.tomcat.initial-size=5

#设置日志的级别,方便我们进行查看
logging.level.org.mybatis=debug
logging.level.com.quan=debug
logging.level.org.springframework=debug

在Spring 数据库事务中可以使用编程式事务,也可以使用声明式事务。大
部分的情况下,会使用声明式事务。编程式事务这种比较底层的方式己经基本被淘汰了, Spring Boot
也不推荐我们使用,

以前JDBC的数据库事务

@Service
public class JdbcServiceImpl implements JdbcService{

@Autowired  
DataSource dataSource = null;

@Override  
public int insertUser(String userName, String note) {  
    Connection connection = null;  
    int result = 0;

    try {  
        connection = dataSource.getConnection();  
        //开启事务  
        connection.setAutoCommit(false);  
        //设置隔离级别  
        connection.setTransactionIsolation(TransactionIsolationLevel.READ\_COMMITTED.getLevel());

 **PreparedStatement ps** **\= connection.prepareStatement("insert into t\_user(user\_name,note) values (?,?)");  
        ps.setString(1,userName);  
        ps.setString(2,note);  
        result =** **ps.executeUpdate();**

        connection.commit();//提交事务  
    } catch (Exception e) {  
        if (connection != null){  
            try {  
                connection.rollback();  
            } catch (SQLException throwables) {  
                throwables.printStackTrace();  
            }  
        }  
    }finally {  
        try {  
            if (connection != null && !connection.isClosed()){  
                connection.close();  
            }  
        } catch (SQLException throwables) {  
            throwables.printStackTrace();  
        }  
    }  
return  result;  
}  

}

上面的业务代码之哟中间的那一段,其他都是JDBC的功能代码,数据库

连接的获取和关闭以及事务的提交和回滚、大量的try… catch … finally . . .语句

我们可以通过使用持久层框架Hibernate,mybatis去减少这些代码,但是还是存在开闭数据库连接和事务控制的代码

上面代码的执行流程:

按照AOP 的设计思想,就可以把除执行SQL 这步之外的步骤
抽取出来单独实现,这便是Spring 数据库事务编程的思想。

Spring声明事务的使用

为了
“擦除”令人厌烦的try… catch … finally .. .语句,减少那些数据库连接开闭和事务回滚提交的代码, Spring
利用其AOP 为我们提供了一个数据库事务的约定流程。通过这个约定流程就可以减少大量的冗余代
码和一些没有必要的町…catch … finally ..语句

对于声明式事务,是使
@Transactional 进行标注的。这个注解可以标注在类或者方法上,当它标注在类上时,代表这个类
所有公共( pub lic )非静态的方法都将启用事务功能。

这个注解的属性等配置内容,在Spring IoC 容器在加载时就会

将这些配置信息解析出来,然后把这些信息存到事务定义器( TransactionDefinition 接口的实现类〉
里, 并且记录哪些类或者方法需要启动事务功能,采取什么策略去执行事务

解析:

当Spring 的上下文开始调用被@Transactional 标注的类或者方法时, Spring 就会产生AOP 的功能。
那么当
它启动事务时,就会根据事务定义器内的配置去设置事务,
11首先是根据传播行为去确定事务的策略

22然后是隔离级别、超时时间、只读等内容的设置,是Spring 事务拦截器根据@Transactional 配置的内容来完成的。

33Spring 就会开始调用开发者编写的业务代码。执行开发者的业务代码,可能发生异常,也可能不发生异常
(Spring 数据库事务的流程中,它会根据是否发生异常采取不同的策略。)

44如果都没有发生异常, Spring 数据库拦截器就会帮助我们自动提交事务

55如果发生异常,就要判断一次事务定义器内的配置,如果事务定义器己经约定了该类型的异常不回
段事务就提交事务, 如果没有任何配置或者不是配置不回滚事务的异常,则会回滚事务,并且将异
常抛出, 这步也是由事务拦截器完成的。

66无论发生异常与否, Spring 都会释放事务资源,这样就可以保证数据库连接池正常可用了,这
也是由S pring 事务拦截器完成的内容。

注解简单使用:

@Override  
@Transactional  
public int insertUser(User user) {  
    return userDao.insertUser(user);  
}

标识insertUser 方法需要启动事务机制

@Transactional源码解析

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
  //指定Beanname,指定事务管理器是哪个
@AliasFor("transactionManager")
String value() default "";
  //和value同样的属性
@AliasFor("value")
String transactionManager() default "";
  //指定传播行为是什么
Propagation propagation() default Propagation.REQUIRED;
//指定隔离级别是什么
Isolation isolation() default Isolation.DEFAULT;
//指定超时时间,单位是秒 int timeout() default -1;
//是否为只读事务 boolean readOnly() default false;
//方法发生指定异常时回滚, 默认所有异常都滚
Class[] rollbackFor() default {};
  //指定异常名称时回滚,默认时所有异常都回滚
String[] rollbackForClassName() default {};
  //方法在发生指定异常时不回滚,默认所有都回滚
Class[] noRollbackFor() default {};
  //方法在发生指定异常名称时不回滚,默认时所有异常都回滚
String[] noRollbackForClassName() default {};
}

value 和transactionManager 属性是配置一个Spring 的事务管理器

timeout 是事务可以允许存在的时间戳,单位为秒;

readOnly 属性定义的是事务是否是只读事务;

rollback.For、rollbackForClassName 、noRollbackFor 和noRollbackForClassName 都是指定异常,
我们从流程中可以看到在带有事务的方法时,可能发生异常,通过这些属性的设置可以指定在什么异常
的情况下依旧提交事务,在什么异常的情况下回滚事务, 这些可以根据自己的需要进行指定。

ropagation 指的是传播行为,

isolation 则是隔离级别

注意:它可以放在接口上,也可以放在实现类上。但是Spring团队推荐放在实现类上,因为放在接口上将使得你的类基于接口的代理时它才生效。

Spring事务管理器

事务的打开、回滚和提交是由事务管理器来完成的。在Spring 中,事务
管理器的顶层接口为PlatformTra ns actionManager

其他框架也会有他们自己的事务管理器的类,

引入Hibernate , 那么Spring orm 包还会提供Hib 巳mateTransactionManager 与之对应并给我们使用

最常用到的事务管理器是DataSourceTransactionManager 。

它实现了接口PlatfonnTransactionManager

接口的源码如下:

public interface PlatformTransactionManager extends TransactionManager {
TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException;

void commit(TransactionStatus var1) throws TransactionException;

void rollback(TransactionStatus var1) throws TransactionException;  

}

getTransaction 方法的参数是一个事务定义器
( Transac tionDefinition ) , 它是依赖于我们配置的@ Transactional 的配置项生成的,于是通过它就能够
设置事务的属性了,而提交和回滚事务也就可以通过commit 和rollback 方法来执行。

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

在Spring Boot 中,当你依赖于mybatis-spring-boot-starter 之后, 它会自动创建一个
DataSourceTransactionManager对象,作为事务管理器,如果依赖于spring-boot-starter-data-jpa ,
则它会自动创建JpaTransactionManager 对象作为事务管理器,所以我们一般不需要自己创建事务管理器而直接
使用它们即可。

简单测试一下数据库事务

实体类

@Alias("user")
public class User {
private Long id;
private String userName;
private String note;

public Long getId() {  
    return id;  
}

public void setId(Long id) {  
    this.id = id;  
}

public String getUserName() {  
    return userName;  
}

public void setUserName(String userName) {  
    this.userName = userName;  
}

public String getNote() {  
    return note;  
}

public void setNote(String note) {  
    this.note = note;  
}  

}

注:使用@ A lias 注解定义了它的别名,这样就可以让My Batis 扫描到其上下文中

Mybatis接口文件

@Repository
public interface UserDao {
User getUser(Long id);
int insertUser(User user);
}

接口映射的文件:



<insert id="insertUser" useGeneratedKeys="true" keyProperty="id" parameterType="user">  
    insert into user (user\_name,note) values (#{userName},#{note})  
</insert>  

因为之前加入了user别名,这里可以直接使用resultType=user就可以了

<ins创〉元素定义的属性useGeneratedKeys 和keyProperty ,则表示在插入之后使用数据库生成
机制回填对象的主键。

服务接口及其实现类

public interface UserService {
User getUser(long id);
int insertUser(User user);
}

###############################################

@Service
public class UserServiceImpl implements UserService{
@Autowired
UserDao userDao = null;

@Override  
@Transactional(isolation = Isolation.READ\_COMMITTED,timeout = 1)//隔离级别,超时  
public User getUser(long id) {  
    return userDao.getUser(id);  
}

@Override  
@Transactional  
public int insertUser(User user) {  
    return userDao.insertUser(user);  
}  

}

测试数据库事务:

@Controller
@RequestMapping("/user")
public class UserController {
@Autowired
UserService userService = null;

@RequestMapping("/getUser/{id}")  
@ResponseBody  
public User getUser(@PathVariable("id") Long id) {  
   return userService.getUser(id);  
}

@RequestMapping("/insertUser")  
@ResponseBody  
public int insertUser(String userName ,String note){  
    User user = new User();  
    user.setNote(note);  
    user.setUserName(userName);

    return userService.insertUser(user);  
}

配置mybatis

mybatis.mapper-locations=classpath:mapper/UserDaoMapper.xml
mybatis.type-aliases-package=com.quan.done.model

依赖于mybatis叩ring-boot”starter 之后, Spring Boot 会自动创建
事务管理器、MyBatis 的S qlSessionFactory 和S qlSessionTemplate 等内容。

可以通过下面配置,

查看Spring Boot 自动为我们创建的事务管理器、
SqlSessionFactory 和Sq ISessionTemplate 信息

//加入扫描对应的包
@MapperScan(basePackages = "com.quan.done",annotationClass = Repository.class)
@SpringBootApplication
//@EnableTransactionManagement
//因为这个自动开启事务其实在@SpringBootApplication里面就已经默认开启了
public class DoneApplication {

public static void main(String\[\] args) {  
    SpringApplication.run(DoneApplication.class, args);

}

//// #################\\
// //注入事务管理器,又springboot自动生成
@Autowired
PlatformTransactionManager transactionManager;
//使用后初始化方法,关策自动生成的事务管理器
@PostConstruct
public void viewTransactionManager(){
System.out.println(transactionManager.getClass().getName());
}
}

注意:

使用了@MapperScan 扫描对应的包, 并限定了只有被注解@Repository 标注的接口

通过注解@Autowired 直接注入了事务管理器, 它是通过S pring Boot 的机制自动生成的,

在viewMy Batis__方法中,加入了注解@PostConstruct , 所以在这个类对象被初始化后,会调用这个方法,__

加入断电可以查看到信息

访问插入产生日志

隔离级别:

数据库的事务知识

事务的基本特征ACID

Atomic (原子性): 事务中包含的操作被看作一个整体的业务单元, 这个业务单元中的操作
要么全部成功,要么全部失败,不会出现部分失败、部分成功的场景。

Consistency (一致性):事务在完成时,必须使所有的数据都保持一致状态,在数据库中所
有的修改都基于事务,保证了数据的完整性。

Isolation (隔离性): 这是我们讨论的核心内容,正如上述,可能多个应用程序线程同时访问
同一数据,这样数据库同样的数据就会在各个不同的事务中被访问,这样会产生丢失更新。
为了压制丢失更新的产生,数据库定义了隔离级别的概念,通过它的选择,可以在不同程度
上压制丢失更新的发生。因为互联网的应用常常面对高并发的场景,所以隔离性是需要掌握
的重点内容。

Durability (持久性):事务结束后,所有的数据会固化到一个地方,如保存到磁盘当中,即
使断电重启后也可以提供给应用程序访问。

什么是第一类丢失更新???

T S 时刻事务l 回滚,导致原本库存为99 的变为了100 ,显然事务2 的结果就丢失了,
这就是一个错误的值。

对于这样一个事务回滚另外一个事务提交而引发的数据不一致的情况,我们称为第一类丢失更新

什么是第二类丢失更新???

T S 时刻提交的事务。因为在事务l 中,无法感知事务2 的操作,这样它就不知道事务2 己经修改过了数据,
因此它依旧认为只是发生了一笔业务, 所以库存变为了99 ,而这个结果又是一个错误的结果。

事务l 提交的事务,就会引发事务2 提交结果的丢失, 我们把这样的多个事务都提交引发的丢失更新称为第二类丢失更新。

第一类和第二类区别在于一个是回滚更新丢失,一个是提交跟新丢失

四种隔离级别

未提交读,读写提交,可重复读,串行化

存在四种级别的意义

一个是数据的一致性, 另一个是性能。数据库现有的
技术完全可以避免丢失更新,但是这样做的代价, 就是付出锁的代价,在互联网中, 系统不单单要
考虑数据的一致性,还要考虑系统的性能。试想,在互联网中使用过多的锁, 一旦出现商品抢购这
样的场景, 必然会导致大量的线程被挂起和恢复,因为使用了锁之后, 一个时刻只能有一个线程访
问数据,这样整个系统就会十分缓慢,当系统被数千甚至数万用户同时访问时,过多的锁就会引发
岩机,大部分用户线程被挂起, 等待持有锁事务的完成, 这样用户体验就会十分糟糕。

注意:一般而言, 互联网系统响应超过5 秒, 就会让用户觉得很不友好, 进而引发用户忠诚度下降的问题。

未提交读 Read uncommitted

是一种最低的隔离级别,危险性级别,实际应用不多,但是性能高

坏处:出现脏读,

脏读的例子:

事务2在事务1扣减了库存,但是没有提交事务的情况下,将扣减的库存再扣减,然后事务2提交,库存为0

但是事务1回滚事务,不在存在第一类丢失更新了,不会回滚到2,库存为0,结果错误了。

读写提交 Read committed

一个事务只能读取另一个事务已经提交的事务,不能读取还没提交完的数据

事务1先进行扣减,此时库存为1,但是没有提交事务,

事务2进行扣减,此时库存为1,但是读取不到事务1未提交的库存数据,所以读的数据应该为2

事务2提交事务,库存为变为1。

事务1进行回滚,回滚为1,

读写提交会出现不可重复读现象

T3的时候,因为是读写提交,所以当事务1的扣减没有提交的时候,事务2只能读原来的库存,获取到的库存为1,准备进行扣减

这时候事务1提交事务,库存为0,此时事务2再进行扣减就没法扣减了

不可重复读现象:

库存对于事务2是一个可变化的值,事务2之前认为是可以扣减,到扣减的时候才发现其实不难扣减了。

可重复读

解决读写提交中出现不可重复读的现象,因为在读写提交的时候,可能出

现一些值的变化, 影响当前事务的执行

事务2进行读取库存的时候,因为事务1正在读取中,所以数据库会对事务2的读取进行阻塞,直到

事务1被提交,事务2才能读取库存的值。

但是!!!会出现幻读

先这里的笔数不是数据库存储的值,而是一个统计值,商品库存则是数据库存储的值,这一点是要注意

的。也就是幻读不是针对一条数据库记录而言,而是多条记录,例如, 这51 笔交易笔数就是多条数 据库记录统计出来的。而可重复读是针对数据库的单一条记录,例如,商品的库存是以数据库里面 的一条记录存储的,它可以产生可重复读,而不能产生幻读。

串行化Serializable

要求SQL按照顺序执行,能够完全保证数据的一致性。

四种隔离级别的对比

追求更高的隔离
级别,它能更好地保证了数据的一致性,但是也要付出锁的代价。有了锁,就意味着性能的丢失,
而且隔离级别越高,性能就越是直线地下降。所以我们在选择隔离级别时,要考虑的不单单是数据
一致性的问题,还要考虑系统性能的问题

一般而言,选择隔离级别会以读写提交为主,它能够防止脏读,而不能避免不可
重复读和幻读。为了克服数据不一致和性能问题,程序开发者还设计了乐观锁,甚至不再使用数据
库而使用其他的手段。例如,使用Redis 作为数据载体

隔离级别的使用

@Transactional注解:

查看这个隔离级别 Isolation枚举类:

public enum Isolation {
DEFAULT(-1),
READ_UNCOMMITTED(1),
READ_COMMITTED(2),
REPEATABLE_READ(4),
SERIALIZABLE(8);

private final int value;

private Isolation(int value) {  
    this.value = value;  
}

public int value() {  
    return this.value;  
}  

}

注意:上面的数字代表是使用springboot配置文件进行配置默认的隔离级别的时候使用的

设置默认隔离级别

在配置文件中

#设置tomcat数据源的默认隔离级别
spring.datasource.tomcat.default-transaction-isolation=2
#设置dbcp2数据源连接池的默认隔离级别
spring.datasource.dbcp2.default-transaction-isolation=2

传播行为:

什么是传播行为???

方法之间调用事务采取的策略问题

在绝大部分的情况下,我们会认为数据库事务要么全部成功, 要么全部失败。但现实中也许会有特殊的情况。
例如,执行一个批量程序,它会处理很多的交易,绝大部分交易是可以顺利完成的,但是也有极少数的交易
因为特殊原因不能完成而发生异常,这时我们不应该因为极少数的交易不能完成而回滚批量任务调用的其他
交易,使得那些本能完成的交易也变为不能完成了。此时,我们真实的需求是

在一个批量任务执行的过程中,调用多个交易时,如果有一些交易发生异常,只是回滚那些出现异常的交易,
而不是整个批量任务,这样就能够使得那些没有问题的交易可以顺利完成,而有问题的交易则不做任何事情,

就是当一个方法调用另一个方法的时候,可以让事务采取不同的策略,新建事务,或者挂起当前事务等

图中,批量任务我们称之为当前方法,那么批量事务就称为当前事务,当它调用单个交易时,称单个交易
为子方法,当前方法调用子方法的时候,让每一个子方法不在当前事务中执行,而是创建一个新的
事务去执行子方法,我们就说当前方法调用子方法的传播行为为新建事务。此外, 还可能让子方法
在无事务、独立事务中执行,这些完全取决于你的业务需求。

7种传播行为

通过枚举类Propagation定义的

package org spr 工ngframework . tra 口saction.an 口otatio 口;

public enum Propagation {
I
*需要事务,它是默认传播行为,如果当前存在事务,就沿用当前事务,
去否则新建一个事务运行子方法
REQUIRED(TransactionDefinition . PROPAGATION_REQUIRED),

*支持事务,如果当前存在事务,就沿用当前事务,
*如果不存在,则继续采用无事务的方式运行子方法

SUPPORTS(Transact 工on Def 工nition . PROPAGATION SUPPORTS) ,
/**
*必须使用事务,如果当前没有事务,则会抛出异常,
如果存在当前事务, 就沿用当前事务

MANDATORY ( TransactionDef 工nition . PROPAGATION MANDATORY) ,

女无论当前事务是否存在,都会创建新事务运行方法,
女这样新事务就可以拥有新的锁和隔离级别等特性,与当前事务相互独立

REQUIRES NEW(TransactionDefinition . PROPAGATION_REQUIRES_NEW),
/**
*不支持事务,当前存在事务时,将挂起事务,运行方法
*/
NOT SUPPORTED(TransactionDefin 工tion . PROPAGATION NOT SUPPORTED),

不支持事务,如果当前方法存在事务,则抛出异常,否则继续使用无事务机制运行

NEVER(Transact 工onDefinition . PROPAGATION NEVER),

*在当前方法调用子方法时,如果子方法发生异常,
只因回滚子方法执行过的SQL ,而不回滚当前方法的事务

NESTED(TransactionDefinition . PROPAGATION_NESTED); private final 工nt value ;
Propagation (in t value) { this value = value ; )
public int value () { return this . value ; )

注意:常用的就加颜色的三种。

public enum Propagation {
REQUIRED(0),
SUPPORTS(1),
MANDATORY(2),
REQUIRES_NEW(3),
NOT_SUPPORTED(4),
NEVER(5),
NESTED(6);

private final int value;

private Propagation(int value) {  
    this.value = value;  
}

public int value() {  
    return this.value;  
}  

}

测试传播行为:

propagation = Propagation.REQUIRED)//沿用当前事务

**建立接口和实现类去批量跟新用户
**UserBatchService

public interface UserBatchService {
public int insertUsers(List userList);
}

UserBatchServiceImpl

@Service
public class UserBatchServiceImpl implements UserBatchService{

@Autowired  
private  UserService userService= null;

@Override  
@Transactional(isolation = Isolation.READ\_COMMITTED,propagation = Propagation.REQUIRED)//沿用当前事务  
public int insertUsers(List<User> userList) {  
    int count =1;  
    for (User u : userList) {  

// 调用子方法,回使用事务注解定义的传播事务
count += userService.insertUser(u);
}
return count;
}
}

增加测试

@Autowired  
UserBatchService userBatchService =null;

@RequestMapping("/insertUsers")  
@ResponseBody  
public int insertUser(String userName1 ,String note1,String userName2 ,String note2){  
    User user1 = new User();  
    user1.setNote(note1);  
    user1.setUserName(userName1);

    User user2 = new User();  
    user2.setUserName(userName2);  
    user2.setNote(note2);

    List<User> userList = new ArrayList<User>();  
    userList.add(user1);  
    userList.add(user2);

    return userBatchService.insertUsers(userList);  
}

日志:

测试传播行为propagation = Propagation.REQUIRES_NEW

@Override  

// @Transactional(isolation = Isolation.READ_COMMITTED,timeout = 1)
@Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRES_NEW)
public int insertUser(User user) {
return userDao.insertUser(user);
}

设置当前的事务隔离类型;

Changing isolation level of JDBC Connection [HikariProxyConnection@1463053521 wrapping com.mysql.cj.jdbc.ConnectionImpl@2619ef67] to 2

设置子方法的事务隔离级别

Suspending current transaction, creating new transaction with name [com.quan.done.jdbc.UserServiceImpl.insertUser]

测试传播行为Propagation.NESTED

子方法回滚,当前事务不会滚,设置savepoint

@Transactional(isolation = Isolation.READ\_COMMITTED,propagation = Propagation.NESTED)  
public int insertUser(User user) {  
    return userDao.insertUser(user);  
}

创建nested事务:

Creating nested transaction with name [com.quan.done.jdbc.UserServiceImpl.insertUser]

设置保存点:

Releasing transaction savepoint

在大部分的数据库中, 一段SQL i吾句中可以设置一个标志位,然后后面的代码执行时如果有异常,只是回滚到这
个标志位的数据状态,而不会让这个标志位之前的代码也回滚。这个标志位,在数据库的概念中被称为保存点
( save point ) 。

注意:并不是所有的数据库都支持保存点技术,因此Spring 内部有这样的规则: 当数据库支持保存点技术时,

就启用保存点技术;如果不能支持,就新建一个事务去运行你的代码,即等价于REQUIRES_NEW 传播行为。

@Transactional失效场景

场景:不新建类,直接类内部进行方法的调用

这是一个类自身方法之间的调用,我们称之为自调用

上述场景是不会出现新的事务的,原因如下;

S pring 数据库事务的约定, 其实现原理是AOP , 而AOP 的原理是动态代理, 在自调用的过程中,
是类自身的调用,而不是代理对象去调用, 那么就不会产生AOP , 这样Spri ng就不能把你的代码织入到约定的流程中