目录

Redisson 分布式 Redis 客户端

分布式锁需求分析 与 主流实现方式

Redisson  分布式锁快速入门

Redisson 分布式锁常用 API

自定义 Redisson  配置选项

YML 文件方式配置(推荐方式)


Redisson 分布式 Redis 客户端

1、Redisson 是一个在 Redis 的基础上实现的 Java 驻内存数据网格(In-Memory Data Grid),它不仅提供了一系列的分布式的 Java 常用对象,还提供了许多分布式服务,如 (BitSet, Set, Multimap, SortedSet, Map, List, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, AtomicLong, CountDownLatch, Publish / Subscribe, Bloom filter, Remote service, Spring cache, Executor service, Live Object service, Scheduler service) 。

2、Redisson 底层采用的是 Netty 框架,支持 Redis 2.8 以上版本,支持 Java1.6+ 以上版本。Redis 命令和 Redisson 对象匹配列表。

3、个人理解:Redisson、Jedis、Lettuce 是三个不同的操作 Redis 的客户端,Jedis、Lettuce 的 API 更侧重对 Reids 数据库的 CRUD(增删改查),而 Redisson API 侧重于分布式开发,比如它的分布式锁。Spring Boot 为 Lettuce 和 Jedis 客户端库提供基本的自动配置,且默认使用 Lettuce 作为客户端,对于现在微服务开发,项目通常都是分布式多实例部署,分布式锁通常都会用到,实现分布式锁的方式有很多,Redisson 解决方案就是其中一种,此时只需要添加 Redisson 依赖即可轻松使用,不用担心与 Jedis 或 Lettuce 冲突。

4、通过 Redisson 官方文档可以发现 Redisson 有很多分布式功能,本文暂时以分布式锁进行练习。

Redisson github 开源地址:https://github.com/redisson/redisson/

Redisson 官方中文文档: 目录 · redisson/redisson Wiki · GitHub

Redisson 官方示例:https://github.com/redisson/redisson-examples

分布式锁需求分析 与 主流实现方式

1、当两个用户同时请求,落在同一个系统的不同节点上时,如果使用 Java 原生的锁机制(synchronized或ReentrantLock ),则是无效的,因为图中的两个 A 系统节点,运行在两个不同的 JVM 中,加的锁只对属于自己 JVM 里面的线程有效。

2、此时,解决办法是使用分布式锁,分布式锁的思路是:为整个系统提供一个全局、唯一的锁。比较常用的是 Redission,以及 基于 zookeeper 实现分布式锁。本文主要介绍前者。

3、Redisson 分布式锁不仅使用非常简单,而且可靠性高:

redisson 所有指令都通过 lua 脚本执行,redis 支持 lua 脚本原子性执行

redisson 设置 key 的默认过期时间为 30s,当某个客户端持有锁超过了30s时怎么办呢?redisson 的 watchdog (看门狗)会在获取锁之后,每隔 10 秒自动将 key 的超时时间设为 30s,所以一直持有锁也不会出现 key 过期。

redisson 的“看门狗”逻辑保证了没有死锁发生,比如机器宕机了,看门狗也就没了,此时自然也就不会延长 key 的过期时间,到了 30s 之后就会自动过期了,其他线程可以获取到锁。

分布式锁主流实现方式
1、基于数据库记录,进入时写数据,退出时删记录
2、数据库行锁,比如分布式 quartz,它是一把排它锁
3、基于 Redis,自己直接使用 redis,或者使用第三方的框架,如 Redisson

4、基于 zookeeper

5、redis 获取锁是轮训机制,客户端每隔一段时间去获取一下,锁释放后会有多个调用者争抢;zk 是监听机制,有变动会接到通知,除了非公平锁,也可以实现公平锁。

Redisson  分布式锁快速入门

1、以如下的示例进行验收分布式锁,当没有锁的情况下,用户重复请求时,后台重复执行业务代码。

2、第一步:项目中导入 Redisson 依赖。Redission 官网分布式锁和同步器

<!-- https://mvnrepository.com/artifact/org.redisson/redisson -->
<dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.13.4</version>
</dependency>

3、第二步:配置 RedissonClient 实例

无论 Reids 是单机部署、还是主从复制、哨兵模式、云托管、集群部署等等,Redisson 都提供了相应的[配置方法]。本节以 redis 服务器单机部署为例。

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/*** Redisson 配置类** @author wangMaoXiong* @version 1.0* @date 2020/9/24 19:28*/
@Configuration
public class RedissonConfig {//redis 服务器单机部署时,创建 RedissonClient 实例,交由 Spring 容器管理@Beanpublic RedissonClient redissonClient() {/*** Config:Redisson 配置基类,SingleServerConfig:单机部署配置类,MasterSlaveServersConfig:主从复制部署配置* SentinelServersConfig:哨兵模式配置,ClusterServersConfig:集群部署配置类。* useSingleServer():初始化 redis 单服务器配置。即 redis 服务器单机部署* setAddress(String address):设置 redis 服务器地址。格式 -- 主机:端口,不写时,默认为 127.0.0.1:6379* setDatabase(int database): 设置连接的 redis 数据库,默认为 0* setPassword(String password):设置 redis 服务器认证密码,没有时设置为 null,默认为 null* RedissonClient create(Config config): 使用提供的配置创建同步/异步 Redisson 实例* Redisson 类实现了 RedissonClient 接口,真正需要使用的就是这两个 API*/Config config = new Config();config.useSingleServer().setAddress("redis://127.0.0.1:6379").setDatabase(2).setPassword(null);RedissonClient redissonClient = Redisson.create(config);return redissonClient;}
}

Config 以及 XxxServerConfig  配置类中提供了许多的配置项,不过通常大部分默认即可,为了演示方便,才写死在代码中,实际中应该是由配置文件配置。

4、第四步:分布式锁使用

加锁之后,对于同一个支付订单,当用户重复请求时,因为执行业务前已经上锁了,所以后续请求必须等待前面的请求执行完成,释放锁之后,才能继续执行。(只是为了演示方便,才在控制层加锁,实际中应该在业务层操作)

    /*** RedissonClient.getLock(String name):可重入锁* boolean tryLock(long waitTime, long leaseTime, TimeUnit unit):尝试获取锁* 1、waitTime:获取锁时的等待时间,超时自动放弃,线程不再继续阻塞,方法返回 false* 2、leaseTime:获取到锁后,指定加锁的时间,超时后自动解锁* 3、如果成功获取锁,则返回 true,否则返回 false。* <p>* http://localhost:8080/redisson/payment3?orderNumber=8856767** @param orderNumber* @return*/@GetMapping("redisson/payment3")public String payment3(@RequestParam Integer orderNumber) {String result = "订单【" + orderNumber + "】支付成功.";logger.info("用户请求支付订单【" + orderNumber + "】.");String key = "com.wmx.wmxredis.controller.RedissonController.payment3_" + orderNumber;/*** getLock(String name):按名称返回锁实例,实现了一个非公平的可重入锁,因此不能保证线程获得顺序* lock():获取锁,如果锁不可用,则当前线程将处于休眠状态,直到获得锁为止*/RLock lock = redissonClient.getLock(key);boolean tryLock = false;try {tryLock = lock.tryLock(30, 180, TimeUnit.SECONDS);} catch (InterruptedException e) {e.printStackTrace();}if (!tryLock) {return "订单【" + orderNumber + "】正在支付中,请耐心等待!";}try {logger.info("查询支付状态");TimeUnit.SECONDS.sleep(40);logger.info("开始支付订单【" + orderNumber + "】");TimeUnit.SECONDS.sleep(40);lock.unlock();} catch (Exception e) {e.printStackTrace();result = "订单【" + orderNumber + "】支付失败:" + e.getMessage();} finally {logger.info("结束支付订单【" + orderNumber + "】");/*** boolean isLocked():检查锁是否被任何线程锁定,被锁定时返回 true,否则返回 false.* unlock():释放锁, Lock 接口的实现类通常会对线程释放锁(通常只有锁的持有者才能释放锁)施加限制,* 如果违反了限制,则可能会抛出(未检查的)异常。如果锁已经被释放,重复释放时,会抛出异常。*/if (lock.isLocked()) {lock.unlock();}}return result;}

在线演示源码:src/main/java/com/wmx/wmxredis/redisson/RedissonController.java · 汪少棠/wmx-redis - Gitee.com

Redisson 分布式锁常用 API

更多详细信息参考官网:分布式锁和同步器

Redisson 类实现 RedissonClient 接口

RLock lock = redisson.getLock("key");
lock.lock();
//业务代码
lock.lock();
//业务代码
lock.unlock();
lock.unlock();

可重入锁(Reentrant Lock)

按名称返回锁实例,实现了一个非公平的可重入锁,因此不能保证线程获得顺序

可重复锁是指可以多次对同一个 key 的进行 lock,加锁几次,同样就得解锁(unlock)几次。

RLock fairLock = redisson.getFairLock("key");

 可重入公平锁(Fair Lock)

保证了当多个 Redisson 客户端线程同时请求加锁时,优先分配给先发出请求的线程。所有请求线程会在一个队列中排队,当某个线程出现宕机时,Redisson 会等待5秒后继续下一个线程

联锁(MultiLock):将多个 RLock 对象关联为一个联锁,每个 RLock 对象实例可以来自于不同的 Redisson 实例。

RLock lock1 = redissonInstance1.getLock("lock1");
RLock lock2 = redissonInstance2.getLock("lock2");
RLock lock3 = redissonInstance3.getLock("lock3");

RedissonMultiLock lock = new RedissonMultiLock(lock1, lock2, lock3);
// 同时加锁:lock1 lock2 lock3
// 所有的锁都上锁成功才算成功。
lock.lock();
...
lock.unlock();

红锁(RedLock):也可以用来将多个 RLock 对象关联为一个红锁,每个RLock对象实例可以来自于不同的Redisson实例,在大部分节点上加锁成功就算成功。

RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);
// 给lock1,lock2,lock3加锁,只要在大部分节点上加锁成功就算成功。如果没有手动解开的话,10秒钟后将会自动解开
lock.lock(10, TimeUnit.SECONDS);

// 为加锁等待100秒时间,并在加锁成功10秒钟后自动解开
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
...
lock.unlock();

读写锁(ReadWriteLock):分布式可重入读写锁允许同时有多个读锁和一个写锁处于加锁状态。

RReadWriteLock rwlock = redisson.getReadWriteLock("anyRWLock");
// 最常见的使用方法
rwlock.readLock().lock();
// 或
rwlock.writeLock().lock();

RLock 接口常用 API
void lock.lock(); 获取锁,如果锁不可用,则当前线程将处于休眠状态,直到获得锁为止
void lock(long var1, TimeUnit var3)

获取锁,如果锁不可用,则当前线程将处于休眠状态,直到获得锁为止。

加锁以后在指定的时间后自动解锁,可以无需再调用 unlock 方法手动解锁

boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) 尝试获取锁,如果成功获取锁,则返回 true,否则返回 false。
waitTime:获取锁时的等待时间,超时则放弃,线程不再继续阻塞,返回 false
leaseTime:获取到锁后,指定加锁的时间,超时后自动解锁
void unlock(); 释放锁, Lock 接口的实现类通常会对线程释放锁(通常只有锁的持有者才能释放锁)施加限制,如果违反了限制,则可能会抛出(未检查的)异常。
注意:如果锁已经被释放了,重复释放时,会抛出异常.
boolean forceUnlock(); 强制释放锁,即使锁已经被释放时,重复强制释放也不会抛出异常!
boolean isLocked() 检查锁是否被任何线程锁定,被锁定时返回 true,否则返回 false.
String getName() 获取锁的名称,即锁的 key.
boolean isHeldByThread(long threadId) 检查锁是否由指定线程Id的线程持有,是则返回 true,否则返回 false
boolean isHeldByCurrentThread() 检查锁是否由当前线程持有,是则返回 true,否则返回 false
long remainTimeToLive();

获取锁的过期时间(毫秒),如果锁不存在,则返回 -2,如果锁存在但没有指定失效时间,则返回 -1。

自定义 Redisson  配置选项

上面的例子中将参数写死在代码中只是为了演示方便,现在优化一下,采用配置文件进行配置。

一:自定义配置属性 src/main/java/com/wmx/wmxredis/redisson/RedssionProperties.java · 汪少棠/wmx-redis - Gitee.com

1、Config 以及 XxxServerConfig 配置类中提供了许多的配置项,不过通常大部分默认即可,可以抽取其中比较常用的选项自定义一个配置类。这些属性可以参考 {@link SingleServerConfig}、{@link BaseConfig}、{@link Config}。

2、这些属性也可以参考 Redssion 官网配置方法。

import org.redisson.config.BaseConfig;
import org.redisson.config.Config;
import org.redisson.config.SingleServerConfig;
import org.springframework.boot.context.properties.ConfigurationProperties;
/*** @author wangMaoXiong* <p>* 1、自定义 Redssion 配置属性,这些属性可以参考 {@link SingleServerConfig}、{@link BaseConfig}、{@link Config},根据需要添加或者减少* 2、Redssion 官网配置方法:https://github.com/redisson/redisson/wiki/2.-%E9%85%8D%E7%BD%AE%E6%96%B9%E6%B3%95*/
@ConfigurationProperties(prefix = "redisson")
public class RedssionProperties {//Redis 服务器地址private String address;//用于Redis连接的数据库索引private int database = 0;//Redis身份验证的密码,如果不需要,则应为nullprivate String password;//Redis最小空闲连接量private int connectionMinimumIdleSize = 24;//Redis连接最大池大小private int connectionPoolSize = 64;//Redis 服务器响应超时时间,Redis 命令成功发送后开始倒计时(毫秒)private int timeout = 3000;//连接到 Redis 服务器时超时时间(毫秒)private int connectTimeout = 10000;//省略 getter、setter 方法未粘贴
}

二:自定义配置类 src/main/java/com/wmx/wmxredis/redisson/RedissonConfig.java · 汪少棠/wmx-redis - Gitee.com

@Configuration
@EnableConfigurationProperties(RedssionProperties.class)
public class RedissonConfig {private final RedssionProperties redssionProperties;/*** 通过构造器从 Spring 容器中获取 {@link RedssionProperties}实例** @param redssionProperties*/public RedissonConfig(RedssionProperties redssionProperties) {this.redssionProperties = redssionProperties;}/*** redis 服务器单机部署时,创建 RedissonClient 实例,交由 Spring 容器管理* 只有当配置了 redisson.type=stand-alone 时,才继续生成 RedissonClient 实例并交由 Spring 容器管理** @return*/@Bean@ConditionalOnProperty(prefix = "redisson", name = "type", havingValue = "stand-alone")public RedissonClient redissonClient() {/*** Config:Redisson 配置基类,SingleServerConfig:单机部署配置类,MasterSlaveServersConfig:主从复制部署配置* SentinelServersConfig:哨兵模式配置,ClusterServersConfig:集群部署配置类。* useSingleServer():初始化 redis 单服务器配置。即 redis 服务器单机部署* setAddress(String address):设置 redis 服务器地址。格式 -- redis://主机:端口,不写时,默认为 redis://127.0.0.1:6379* setDatabase(int database): 设置连接的 redis 数据库,默认为 0* setPassword(String password):设置 redis 服务器认证密码,没有时设置为 null,默认为 null* RedissonClient create(Config config): 使用提供的配置创建同步/异步 Redisson 实例* Redisson 类实现了 RedissonClient 接口,真正需要使用的就是这两个 API*/Config config = new Config();config.useSingleServer().setAddress(redssionProperties.getAddress()).setDatabase(redssionProperties.getDatabase()).setPassword(redssionProperties.getPassword()).setConnectionPoolSize(redssionProperties.getConnectionPoolSize()).setConnectionMinimumIdleSize(redssionProperties.getConnectionMinimumIdleSize()).setTimeout(redssionProperties.getTimeout()).setConnectTimeout(redssionProperties.getConnectTimeout());RedissonClient redissonClient = Redisson.create(config);return redissonClient;}
}

三:全局 src/main/resources/application.yml · 汪少棠/wmx-redis - Gitee.com


#自定义分布式 Redis 客户端 Redisson 配置
redisson:type: stand-alone  #redis服务器部署类型,stand-alone:单机部署、cluster:机器部署.address: redis://127.0.0.1:6379 #redis服务器地址,单机时必须是redis://开头.

本文虽然只是演示了 Redis 单机部署模式,但是其他 Redis 模式部署时也是同理,都可以参考 Redisson 配置方法 修改可得。

YML 文件方式配置(推荐方式)

1、将配置信息写死在程序代码里显然是不合适的,上面的自定义配置虽然也是一种很好的解决方式,但是官方提供了更好的办法,可以直接从 yml 配置文件中读取配置(推荐方式)。

2、先提供一个 yml 文件,比如 redisson-config.yml,文件名称随意,下面是单机版配置,其它方式配置可以参考官网,比如 集群模式。

# org.redisson.Config类的配置参数,适用于所有Redis组态模式(单机,集群和哨兵)
threads: 0
nettyThreads: 0
codec: !<org.redisson.codec.JsonJacksonCodec> {}
transportMode: NIO# Redis 单机部署时 Redisson 文件方式配置
singleServerConfig:address: "redis://127.0.0.1:6379" #节点地址password: null #密码,默认nulldatabase: 15 #数据库编号,默认0idleConnectionTimeout: 10000 #连接空闲超时,单位毫秒,默认10000connectTimeout: 10000 #连接超时,单位毫秒,默认10000timeout: 3000 #命令等待超时,单位毫秒,默认3000retryAttempts: 3 #命令失败重试次数,默认3retryInterval: 1500 #命令重试发送时间间隔,单位毫秒,默认1500subscriptionsPerConnection: 5 #单个连接最大订阅数量,默认5clientName: null #客户端名称,默认null,subscriptionConnectionMinimumIdleSize: 1 #发布和订阅连接的最小空闲连接数,默认1subscriptionConnectionPoolSize: 50 #发布和订阅连接池大小,默认50connectionMinimumIdleSize: 32 #最小空闲连接数,默认32connectionPoolSize: 64 #连接池大小,默认64dnsMonitoringInterval: 5000 #DNS监测时间间隔,单位毫秒,默认5000sslEnableEndpointIdentification: true #启用SSL终端识别,默认truesslProvider: JDK #SSL实现方式,默认JDKsslTruststore: null #SSL信任证书库路径,默认nullsslTruststorePassword: null #SSL信任证书库密码,默认nullsslKeystore: null #SSL钥匙库路径,默认nullsslKeystorePassword: null #SSL钥匙库密码,默认null

src/main/resources/redisson-config.yml · 汪少棠/wmx-redis - Gitee.com

3、然后在配置类中直接读取即可,这样无论 redis 是怎样部署,只需要修改配置文件即可,完全不用修改任何代码,推荐方式。

    /*** Config fromYAML :从 yaml 文件读取 redisson 配置对象* String toYAML() :将当前配置转换为YAML格式*/@Beanpublic RedissonClient redissonClient() throws IOException {URL resource = RedissonConfig2.class.getClassLoader().getResource("redisson-config.yml");Config config = Config.fromYAML(resource);RedissonClient redissonClient = Redisson.create(config);log.info("Redisson 配置:{}", config.toYAML());return redissonClient;}

src/main/java/com/wmx/wmxredis/redisson/RedissonConfig2.java · 汪少棠/wmx-redis - Gitee.com

4、原生 org.redisson.redisson 依赖就已经提供了 fromYAML 的功能。

Redis 分布式客户端 Redisson 分布式锁快速入门相关推荐

  1. Redisson分布式锁快速入门教程

    清明在家无事,并且因为上海疫情原因只能宅在家里,突然想到之前计划着写一篇Redisson的分布式锁快速入门教程,自己平常在工作中也只能简单会使用,所以文章可能写的比较简单,希望大佬勿喷.此文章也作为个 ...

  2. Redis学习笔记①基础篇_Redis快速入门

    若文章内容或图片失效,请留言反馈.部分素材来自网络,若不小心影响到您的利益,请联系博主删除. 资料链接:https://pan.baidu.com/s/1189u6u4icQYHg_9_7ovWmA( ...

  3. 香饽饽:腾讯强推的Redis天花板笔记,帮助初学者快速入门和提高(核心笔记+面试高频解析)

    前言 在目前的技术选型中,Redis 俨然已经成为了系统高性能缓存方案的事实标准,因此现在 Redis 也成为了后端开发的基本技能树之一. 基于上述情况,今天给大家分享一份我亲笔撰写的阿里内部< ...

  4. php 客户端 mina,Apache Mina快速入门

    Mina是什么 Mina是一个基于NIO的网络框架,使用它编写程序时,可以专注于业务处理,而不用过于关心IO操作.不论应用程序采用什么协议(TCP.UDP)或者其它的,Mina提供了一套公用的接口,来 ...

  5. es高级客户端聚合查询api快速入门

    //聚合查询@Testvoid Collection_query() throws IOException {SearchRequest searchRequest = new SearchReque ...

  6. Redis实战——Redisson分布式锁

    目录 1 基于Redis中setnx方法的分布式锁的问题 2 Redisson 2.1 什么是Redisson 2.2 Redisson实现分布式锁快速入门 2.3 Redisson 可重入锁原理 什 ...

  7. 分布式锁-Redisson快速入门

    分布式锁-Redisson快速入门 一.引入依赖 二.配置Redisson客户端 三.使用Redisson的分布式锁 一.引入依赖 <dependency><groupId>o ...

  8. Redis实现分布式锁全局锁—Redis客户端Redisson中分布式锁RLock实现

    2019独角兽企业重金招聘Python工程师标准>>> 1. 前因 以前实现过一个Redis实现的全局锁, 虽然能用, 但是感觉很不完善, 不可重入, 参数太多等等. 最近看到了一个 ...

  9. redisson redlock(基于redisson框架和redis集群使用分布式锁)

    一.关于分布式锁的两篇文章 文章1 文章2 二.redis分布式锁存在的问题 redis实现分布式锁有很多种方案,比较完善的方案应该是用setNx + lua进行实现.简单实现如下: java代码-加 ...

  10. 分布式锁-Redis解决方案和Redisson解决方案

    文章目录 1:分布式锁的概念 1:概念 2:锁/分布式锁/事务区别 2:本文使用的案例场景 1:需求 2:controller层代码 3:锁控制层代码(使用synchronized 不成功) 4:调用 ...

最新文章

  1. docker(3)docker下的centos7下安装jdk
  2. SpringBoot笔记1-使用idea创建SpringBoot的hello world
  3. java tomcat日志中文乱码问题解决
  4. PHP远程下载图片损坏问题
  5. C语言不使用结构体实现链表,不用指针链表和结构体数组怎么编学生成绩管理系统啊...
  6. AngularJS select中ngOptions用法详解
  7. ES6 javascript 实用开发技巧
  8. 嵌入式Linux系统编程学习之十六用程序发送信号
  9. 世粮署:马斯克、贝索斯等富豪应捐出部分资产缓解全球饥饿
  10. 测试面试集-Python接口自动化测试
  11. Listener 快速开始
  12. 2016大学里的流年回忆
  13. LabWindows/CVI入门之第一章:LabWindows/CVI开发环境
  14. 手撸Mybatis源码-基础版
  15. MPEG音频编码三十年
  16. (遇到问题) AAAI2021 pdf要求: CYMK颜色空间,png图片300DPI,字体嵌入pdf
  17. 谷歌浏览器拓展及脚本安装入门简介
  18. Cesium,ClippingPlanes,任意剪裁面对3DTiles剪裁
  19. Android 下简单的 MP3 播放(代码分析)
  20. 口袋linux设备,口袋中的Linux

热门文章

  1. 孙鑫VC学习笔记:第七讲 对话框
  2. 拓端tecdat|把握出租车行驶的数据脉搏 :出租车轨迹数据给你答案!
  3. js基础知识汇总13
  4. xgboost4j jar包下载
  5. 使用Python进行多项式Lo​​gistic回归
  6. 在线编程无法在sublime中使用input()和raw_input()的解决方法
  7. Python读取文件时出现UnicodeDecodeError: ‘gbk‘ codec can‘t decode byte 0x80 in position ...
  8. 复旦大学计算机a类专业,如何看待浙大A类学科39个,全国第一,录取分却比复旦、上交低?...
  9. 如何在IDEA中搭建SpringMVC?
  10. spark启动的worker节点是localhost_「Spark源码分析1」Spark standalone模式Master和Worker启动流程...