- 浏览: 105956 次
- 性别:
- 来自: 北京
最新评论
概述
==
互联网应用发展到今天,从单体应用架构到SOA以及今天的微服务,随着微服务化的不断升级进化,服务和服务之间的稳定性变得越来越重要,分布式系统之所以复杂,主要原因是分布式系统需要考虑到网络的延时和不可靠,微服务很重要的一个特质就是需要保证服务幂等,保证幂等性很重要的前提需要分布式锁控制并发,同时缓存、降级和限流是保护微服务系统运行稳定性的三大利器。
随着业务不断的发展,按业务域的划分子系统越来越多,每个业务系统都需要缓存、限流、分布式锁、幂等工具组件,distributed-tools组件(暂未开源)正式包含了上述分布式系统所需要的基础功能组件。
distributed-tools组件基于tair、redis分别提供了2个springboot starter,使用起来非常简单。
以使用缓存使用redis为例,application.properties添加如下配置
```
redis.extend.hostName=127.0.0.1
redis.extend.port=6379
redis.extend.password=pwdcode
redis.extend.timeout=10000
redis.idempotent.enabled=true
```
![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "点击并拖拽以移动")
接下来的篇幅,重点会介绍一下缓存、限流、分布式锁、幂等的使用方式。
缓存
==
缓存的使用可以说无处不在,从应用请求的访问路径来看,用户user -> 浏览器缓存 -> 反向代理缓存-> WEB服务器缓存 -> 应用程序缓存 -> 数据库缓存等,几乎每条链路都充斥着缓存的使用,缓存最直白的解释就是“用空间换时间”的算法。缓存就是把一些数据暂时存放于某些地方,可能是内存,也有可能硬盘。总之,目的就是为了避免某些耗时的操作。我们常见的耗时的操作,比如数据库的查询、一些数据的计算结果,或者是为了减轻服务器的压力。其实减轻压力也是因查询或计算,虽然短耗时,但操作很频繁,累加起来也很长,造成严重排队等情况,服务器抗不住。
distributed-tools组件提供了一个CacheEngine接口,基于Tair、Redis分别有不同的实现,具体CacheEngine定义如下:
```
public String get(String key);
/**
* 获取指定的key对应的对象,异常也会返回null
*
* @param key
* @param clazz
* @return
*/
public <T> T get(String key, Class<T> clz);
/**
* 存储缓存数据,忽略过期时间
*
* @param key
* @param value
* @return
*/
public <T extends Serializable> boolean put(String key, T value);
/**
* 存储缓存数据
*
* @param key
* @param value
* @param expiredTime
* @param unit
* @return
*/
public <T extends Serializable> boolean put(String key, T value, int expiredTime, TimeUnit unit);
/**
* 基于key删除缓存数据
*
* @param key
* @return
*/
public boolean invalid(String key);
```
![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "点击并拖拽以移动")
get方法针对key进行查询,put存储缓存数据,invalid删除缓存数据。
限流
==
在分布式系统中,尤其面对一些秒杀、瞬时高并发场景,都需要进行一些限流措施,保证系统的高可用。通常来说限流的目的是通过对并发访问/请求进行限速,或者一个时间窗口内的的请求进行限速来保护系统,一旦达到限制速率则可以 拒绝服务(定向到错误页或告知资源没有了)、排队 或 等待(比如秒杀、评论、下单)、降级(返回托底数据或默认数据,如商品详情页库存默认有货)。
常见的一些限流算法包括固定窗口、滑动窗口、漏桶、令牌桶,distributed-tools组件目前基于计数器只实现了固定窗口算法,具体使用方式如下:
```
/**
* 指定过期时间自增计数器,默认每次+1,非滑动窗口
*
* @param key 计数器自增key
* @param expireTime 过期时间
* @param unit 时间单位
* @return
*/
public long incrCount(String key, int expireTime, TimeUnit unit);
/**
* 指定过期时间自增计数器,单位时间内超过最大值rateThreshold返回true,否则返回false
*
* @param key 限流key
* @param rateThreshold 限流阈值
* @param expireTime 固定窗口时间
* @param unit 时间单位
* @return
*/
public boolean rateLimit(final String key, final int rateThreshold, int expireTime, TimeUnit unit);
```
![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "点击并拖拽以移动")
基于CacheEngine的rateLimit方法可以实现限流,expireTime只能设定固定窗口时间,非滑动窗口时间。
另外distributed-tools组件提供了模板RateLimitTemplate可以简化限流的易用性,可以直接调用RateLimitTemplate的execute方法处理限流问题。
```
/**
* @param limitKey 限流KEY
* @param resultSupplier 回调方法
* @param rateThreshold 限流阈值
* @param limitTime 限制时间段
* @param blockDuration 阻塞时间段
* @param unit 时间单位
* @param errCodeEnum 指定限流错误码
* @return
*/
public <T> T execute(String limitKey, Supplier<T> resultSupplier, long rateThreshold, long limitTime,
long blockDuration, TimeUnit unit, ErrCodeEnum errCodeEnum) {
boolean blocked = tryAcquire(limitKey, rateThreshold, limitTime, blockDuration, unit);
if (errCodeEnum != null) {
AssertUtils.assertTrue(blocked, errCodeEnum);
} else {
AssertUtils.assertTrue(blocked, ExceptionEnumType.ACQUIRE_LOCK_FAIL);
}
return resultSupplier.get();
}
```
![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "点击并拖拽以移动")
另外distributed-tools组件还提供了注解@RateLimit的使用方式,具体注解RateLimit定义如下:
```
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface RateLimit {
/**
* 限流KEY
*/
String limitKey();
/**
* 允许访问的次数,默认值MAX_VALUE
*/
long limitCount() default Long.MAX_VALUE;
/**
* 时间段
*/
long timeRange();
/**
* 阻塞时间段
*/
long blockDuration();
/**
* 时间单位,默认为秒
*/
TimeUnit timeUnit() default TimeUnit.SECONDS;
}
```
![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "点击并拖拽以移动")
基于注解的方式限流使用代码如下:
```
@RateLimit(limitKey = "#key", limitCount = 5, timeRange = 2, blockDuration = 3, timeUnit = TimeUnit.MINUTES)
public String testLimit2(String key) {
..........
return key;
}
```
![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "点击并拖拽以移动")
任何方法添加上述注解具备了一定的限流能力(具体方法需要在spring aop指定拦截范围内),如上代码表示以参数key作为限流key,每2分钟请求次数不超过5次,超过限制后阻塞3分钟。
分布式锁
====
在Java单一进程中通过synchronized关键字和ReentrantLock可重入锁可以实现在多线程环境中控制对资源的并发访问,通常本地的加锁往往不能满足我们的需要,我们更多的面对场景是分布式系统跨进程的锁,简称为分布式锁。分布式锁实现手段通常是将锁标记存在内存中,只是该内存不是某个进程分配的内存而是公共内存如Redis、Tair,至于利用数据库、文件等做锁与单机的实现是一样的,只要保证标记能互斥就行。分布式锁相对单机进程的锁之所以复杂,主要原因是分布式系统需要考虑到网络的延时和不可靠。
distributed-tools组件提供的分布式锁要具备如下特性:
**互斥性:**同本地锁一样具有互斥性,但是分布式锁需要保证在不同节点进程的不同线程的互斥。
**可重入性:**同一个节点上的同一个线程如果获取了锁之后那么也可以再次获取这个锁。
**锁超时:**和本地锁一样支持锁超时,防止死锁,通过异步心跳demon线程刷新过期时间,防止特殊场景(如FGC死锁超时)下死锁。
**高性能、高可用:**加锁和解锁需要高性能,同时也需要保证高可用防止分布式锁失效,可以增加降级。
**支持阻塞和非阻塞:**同ReentrantLock一样支持lock和trylock以及tryLock(long timeOut)。
**公平锁和非公平锁(不支持)**:公平锁是按照请求加锁的顺序获得锁,非公平锁就相反是无序的,目前distributed-tools组件提供的分布式锁不支持该特性。
distributed-tools组件提供的分布式锁,使用起来非常简单,提供了一个分布式锁模板:DistributedLockTemplate,可以直接调用模板提供的静态方法(如下):
```
/**
* 分布式锁处理模板执行器
*
* @param lockKey 分布式锁key
* @param resultSupplier 分布式锁处理回调
* @param waitTime 锁等待时间
* @param unit 时间单位
* @param errCodeEnum 指定特殊错误码返回
* @return
*/
public static <T> T execute(String lockKey, Supplier<T> resultSupplier, long waitTime, TimeUnit unit,
ErrCodeEnum errCodeEnum) {
AssertUtils.assertTrue(StringUtils.isNotBlank(lockKey), ExceptionEnumType.PARAMETER_ILLEGALL);
boolean locked = false;
Lock lock = DistributedReentrantLock.newLock(lockKey);
try {
locked = waitTime > 0 ? lock.tryLock(waitTime, unit) : lock.tryLock();
} catch (InterruptedException e) {
throw new RuntimeException(String.format("lock error,lockResource:%s", lockKey), e);
}
if (errCodeEnum != null) {
AssertUtils.assertTrue(locked, errCodeEnum);
} else {
AssertUtils.assertTrue(locked, ExceptionEnumType.ACQUIRE_LOCK_FAIL);
}
try {
return resultSupplier.get();
} finally {
lock.unlock();
}
}
```
![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "点击并拖拽以移动")
幂等
==
在分布式系统设计中幂等性设计中十分重要的,尤其在复杂的微服务中一套系统中包含了多个子系统服务,而一个子系统服务往往会去调用另一个服务,而服务调用服务无非就是使用RPC通信或者restful,分布式系统中的网络延时或中断是避免不了的,通常会导致服务的调用层触发重试。具有这一性质的接口在设计时总是秉持这样的一种理念:调用接口发生异常并且重复尝试时,总是会造成系统所无法承受的损失,所以必须阻止这种现象的发生。
幂等通常会有两个维度:
1\. 空间维度上的幂等,即幂等对象的范围,是个人还是机构,是某一次交易还是某种类型的交易。
2\. 时间维度上的幂等,即幂等的保证时间,是几个小时、几天还是永久性的。
在实际系统中有很多操作,不管操作多少次,都应该产生一样的效果或返回相同的结果。以下这些应用场景也是通常比较常见的应用场景:
1\. 前端重复提交请求,且请求数据相同时,后台需要返回对应这个请求的相同结果。
2\. 发起一次支付请求,支付中心应该只扣用户账户一次钱,当遇到网络中断或系统异常时,也应该只扣一次钱。
3\. 发送消息,同样内容的短信发给用户只发一次。
4\. 创建业务订单,一次业务请求只能创建一个,重试请求创建多个就会出大问题。
5\. 基于msgId的消息幂等处理
在正式使用distributed-tools组件提供的幂等之前,我们先看下distributed-tools幂等组件的设计。
![1568168260336_1e02e9d6_5c23_4906_9488_9dc900ac5007](https://yqfile.alicdn.com/526041706bd1b5b773dee30c41dffbd16a2954b8.png)
* **幂等key提取能力:获取唯一幂等key**
幂等key的提取支持2中注解:IdempotentTxId、IdempotentTxIdGetter,任意方法添加以上2注解,即可提取到相关幂等key,前提条件是需要将Idempotent注解添加相关需要幂等的方法上。
如果单纯使用幂等模板进行业务处理,需要自己设置相关幂等key,且要保证其唯一性。
* **分布式锁服务能力:提供全局加锁、解锁的能力**
distributed-tools幂等组件需要使用自身提供的分布式锁功能,保证其并发唯一性,distributed-tools提供的分布式锁能够提供其可靠、稳定的加锁、解锁能力。
* **高性能的写入、查询能力:针对幂等结果查询与存储**
distributed-tools幂等组件提供了基于tair、redis的存储实现,同时支持自定义一级、二级存储通过spring依赖注入到IdempotentService,建议distributed-tools幂等存储结果一级存储tair mdb,二级存储ldb或者tablestore,一级存储保证其高性能,二级存储保证其可靠性。
二级存储并行查询会返回查询最快的幂等结果。
二级存储并行异步写入,进一步提高性能。
* **高可用的幂等写入、查询能力:幂等存储出现异常,不影响业务正常流程,增加容错**
distributed-tools幂等组件支持二级存储,为了保证其高可用,毕竟二级存储出现故障的概率太低,不会导致业务上不可用,如果二级存储同时出现故障,业务上做了一定的容错,针对不确定性的异常采取重试策略,会执行具体幂等方法。
一级存储与二级存储的写入与查询处理进行隔离,任何一级存储的异常不会影响整体业务执行。
在了解了distributed-tools组件幂等之后,接下来我们来看下如何去使用幂等组件,首先了解下common-api提供的幂等注解,具体幂等注解使用方式如下:
注解定义
使用范围
使用描述
Idempotent
方法
Idempotent需要定义到具体Method上。Idempotent有个属性定义:
expireDate表示幂等有效期,默认30天。
spelKey表示可以使用spring表达式生成幂等唯一ID,比如直接获取到对象属性或者方法或者其他表达式。
IdempotentTxId
参数、对象属性
IdempotentTxId可以直接定义到方法参数或者参数对象属性上,直接获取幂等ID
IdempotentTxIdGetter
方法
IdempotentTxIdGetter可以直接定义参数对象的方法上,调用该方法获取幂等ID
幂等拦截器获取幂等ID的优先级:
1. 首先判断Idempotent的spelKey的属性是否为空,如果不为空会根据spelKey定义的spring表达式生成幂等ID。
2. 其次判断参数是否包含IdempotentTxId注解,如果有IdempotentTxId,会直接获取参数值生成幂等ID。
3. 再次通过反射获取参数对象属性是否包含IdempotentTxId注解,如果对象属性包含IdempotentTxId注解会获取该参数对象属性生成幂等ID。
4. 最后以上三种情况仍未获取到幂等ID,会进一步通过反射获取参数对象的Method是否定义IdempotentTxIdGetter注解,如果包含该注解则通过反射生成幂等ID。
代码使用示例:
```
@Idempotent(spelKey = "#request.requestId", firstLevelExpireDate = 7,secondLevelExpireDate = 30)
public void execute(BizFlowRequest request) {
..................
}
```
![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "点击并拖拽以移动")
如上述代码表示从request获取requestId作为幂等key,一级存储有效期7天,二级存储有效期30天。
distributed-tools除了可以使用幂等注解外,幂等组件还提供了一个通用幂等模板IdempotentTemplate,使用幂等模板的前提必须设置tair.idempotent.enabled=true或者redis.idempotent.enabled=true,默认为false,同时需要指定幂等结果一级存储,幂等结果存储为可选项配置。
具体使用幂等模板IdempotentTemplate的方法如下:
```
/**
* 幂等模板处理器
*
* @param request 幂等Request信息
* @param executeSupplier 幂等处理回调function
* @param resultPreprocessConsumer 幂等结果回调function 可以对结果做些预处理
* @param ifResultNeedIdempotence 除了根据异常还需要根据结果判定是否需要幂等性的场景可以提供此参数
* @return
*/
public R execute(IdempotentRequest<P> request, Supplier<R> executeSupplier,
Consumer<IdempotentResult<P, R>> resultPreprocessConsumer, Predicate<R> ifResultNeedIdempotence) {
........
}
```
![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "点击并拖拽以移动")
**request:**
幂等参数IdempotentRequest组装,可以设置幂等参数和幂等唯一ID
**executeSupplier:**
具体幂等的方法逻辑,比如针对支付、下单接口,可以通过JDK8函数式接口Supplier Callback进行处理。
**resultBiConsumer:**
幂等返回结果的处理,该参数可以为空,如果为空采取默认的处理,根据幂等结果,如果成功、不可重试的异常错误码,直接返回结果,如果失败可重试异常错误码,会进行重试处理。
如果该参数值不为空,可以针对返回幂等结果进行特殊逻辑处理设置ResultStatus(ResultStatus包含三种状态包括成功、失败可重试、失败不可重试)。
[原文链接](https://yq.aliyun.com/articles/727048?utm_content=g_1000090492)
本文为云栖社区原创内容,未经允许不得转载。
==
互联网应用发展到今天,从单体应用架构到SOA以及今天的微服务,随着微服务化的不断升级进化,服务和服务之间的稳定性变得越来越重要,分布式系统之所以复杂,主要原因是分布式系统需要考虑到网络的延时和不可靠,微服务很重要的一个特质就是需要保证服务幂等,保证幂等性很重要的前提需要分布式锁控制并发,同时缓存、降级和限流是保护微服务系统运行稳定性的三大利器。
随着业务不断的发展,按业务域的划分子系统越来越多,每个业务系统都需要缓存、限流、分布式锁、幂等工具组件,distributed-tools组件(暂未开源)正式包含了上述分布式系统所需要的基础功能组件。
distributed-tools组件基于tair、redis分别提供了2个springboot starter,使用起来非常简单。
以使用缓存使用redis为例,application.properties添加如下配置
```
redis.extend.hostName=127.0.0.1
redis.extend.port=6379
redis.extend.password=pwdcode
redis.extend.timeout=10000
redis.idempotent.enabled=true
```
![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "点击并拖拽以移动")
接下来的篇幅,重点会介绍一下缓存、限流、分布式锁、幂等的使用方式。
缓存
==
缓存的使用可以说无处不在,从应用请求的访问路径来看,用户user -> 浏览器缓存 -> 反向代理缓存-> WEB服务器缓存 -> 应用程序缓存 -> 数据库缓存等,几乎每条链路都充斥着缓存的使用,缓存最直白的解释就是“用空间换时间”的算法。缓存就是把一些数据暂时存放于某些地方,可能是内存,也有可能硬盘。总之,目的就是为了避免某些耗时的操作。我们常见的耗时的操作,比如数据库的查询、一些数据的计算结果,或者是为了减轻服务器的压力。其实减轻压力也是因查询或计算,虽然短耗时,但操作很频繁,累加起来也很长,造成严重排队等情况,服务器抗不住。
distributed-tools组件提供了一个CacheEngine接口,基于Tair、Redis分别有不同的实现,具体CacheEngine定义如下:
```
public String get(String key);
/**
* 获取指定的key对应的对象,异常也会返回null
*
* @param key
* @param clazz
* @return
*/
public <T> T get(String key, Class<T> clz);
/**
* 存储缓存数据,忽略过期时间
*
* @param key
* @param value
* @return
*/
public <T extends Serializable> boolean put(String key, T value);
/**
* 存储缓存数据
*
* @param key
* @param value
* @param expiredTime
* @param unit
* @return
*/
public <T extends Serializable> boolean put(String key, T value, int expiredTime, TimeUnit unit);
/**
* 基于key删除缓存数据
*
* @param key
* @return
*/
public boolean invalid(String key);
```
![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "点击并拖拽以移动")
get方法针对key进行查询,put存储缓存数据,invalid删除缓存数据。
限流
==
在分布式系统中,尤其面对一些秒杀、瞬时高并发场景,都需要进行一些限流措施,保证系统的高可用。通常来说限流的目的是通过对并发访问/请求进行限速,或者一个时间窗口内的的请求进行限速来保护系统,一旦达到限制速率则可以 拒绝服务(定向到错误页或告知资源没有了)、排队 或 等待(比如秒杀、评论、下单)、降级(返回托底数据或默认数据,如商品详情页库存默认有货)。
常见的一些限流算法包括固定窗口、滑动窗口、漏桶、令牌桶,distributed-tools组件目前基于计数器只实现了固定窗口算法,具体使用方式如下:
```
/**
* 指定过期时间自增计数器,默认每次+1,非滑动窗口
*
* @param key 计数器自增key
* @param expireTime 过期时间
* @param unit 时间单位
* @return
*/
public long incrCount(String key, int expireTime, TimeUnit unit);
/**
* 指定过期时间自增计数器,单位时间内超过最大值rateThreshold返回true,否则返回false
*
* @param key 限流key
* @param rateThreshold 限流阈值
* @param expireTime 固定窗口时间
* @param unit 时间单位
* @return
*/
public boolean rateLimit(final String key, final int rateThreshold, int expireTime, TimeUnit unit);
```
![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "点击并拖拽以移动")
基于CacheEngine的rateLimit方法可以实现限流,expireTime只能设定固定窗口时间,非滑动窗口时间。
另外distributed-tools组件提供了模板RateLimitTemplate可以简化限流的易用性,可以直接调用RateLimitTemplate的execute方法处理限流问题。
```
/**
* @param limitKey 限流KEY
* @param resultSupplier 回调方法
* @param rateThreshold 限流阈值
* @param limitTime 限制时间段
* @param blockDuration 阻塞时间段
* @param unit 时间单位
* @param errCodeEnum 指定限流错误码
* @return
*/
public <T> T execute(String limitKey, Supplier<T> resultSupplier, long rateThreshold, long limitTime,
long blockDuration, TimeUnit unit, ErrCodeEnum errCodeEnum) {
boolean blocked = tryAcquire(limitKey, rateThreshold, limitTime, blockDuration, unit);
if (errCodeEnum != null) {
AssertUtils.assertTrue(blocked, errCodeEnum);
} else {
AssertUtils.assertTrue(blocked, ExceptionEnumType.ACQUIRE_LOCK_FAIL);
}
return resultSupplier.get();
}
```
![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "点击并拖拽以移动")
另外distributed-tools组件还提供了注解@RateLimit的使用方式,具体注解RateLimit定义如下:
```
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface RateLimit {
/**
* 限流KEY
*/
String limitKey();
/**
* 允许访问的次数,默认值MAX_VALUE
*/
long limitCount() default Long.MAX_VALUE;
/**
* 时间段
*/
long timeRange();
/**
* 阻塞时间段
*/
long blockDuration();
/**
* 时间单位,默认为秒
*/
TimeUnit timeUnit() default TimeUnit.SECONDS;
}
```
![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "点击并拖拽以移动")
基于注解的方式限流使用代码如下:
```
@RateLimit(limitKey = "#key", limitCount = 5, timeRange = 2, blockDuration = 3, timeUnit = TimeUnit.MINUTES)
public String testLimit2(String key) {
..........
return key;
}
```
![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "点击并拖拽以移动")
任何方法添加上述注解具备了一定的限流能力(具体方法需要在spring aop指定拦截范围内),如上代码表示以参数key作为限流key,每2分钟请求次数不超过5次,超过限制后阻塞3分钟。
分布式锁
====
在Java单一进程中通过synchronized关键字和ReentrantLock可重入锁可以实现在多线程环境中控制对资源的并发访问,通常本地的加锁往往不能满足我们的需要,我们更多的面对场景是分布式系统跨进程的锁,简称为分布式锁。分布式锁实现手段通常是将锁标记存在内存中,只是该内存不是某个进程分配的内存而是公共内存如Redis、Tair,至于利用数据库、文件等做锁与单机的实现是一样的,只要保证标记能互斥就行。分布式锁相对单机进程的锁之所以复杂,主要原因是分布式系统需要考虑到网络的延时和不可靠。
distributed-tools组件提供的分布式锁要具备如下特性:
**互斥性:**同本地锁一样具有互斥性,但是分布式锁需要保证在不同节点进程的不同线程的互斥。
**可重入性:**同一个节点上的同一个线程如果获取了锁之后那么也可以再次获取这个锁。
**锁超时:**和本地锁一样支持锁超时,防止死锁,通过异步心跳demon线程刷新过期时间,防止特殊场景(如FGC死锁超时)下死锁。
**高性能、高可用:**加锁和解锁需要高性能,同时也需要保证高可用防止分布式锁失效,可以增加降级。
**支持阻塞和非阻塞:**同ReentrantLock一样支持lock和trylock以及tryLock(long timeOut)。
**公平锁和非公平锁(不支持)**:公平锁是按照请求加锁的顺序获得锁,非公平锁就相反是无序的,目前distributed-tools组件提供的分布式锁不支持该特性。
distributed-tools组件提供的分布式锁,使用起来非常简单,提供了一个分布式锁模板:DistributedLockTemplate,可以直接调用模板提供的静态方法(如下):
```
/**
* 分布式锁处理模板执行器
*
* @param lockKey 分布式锁key
* @param resultSupplier 分布式锁处理回调
* @param waitTime 锁等待时间
* @param unit 时间单位
* @param errCodeEnum 指定特殊错误码返回
* @return
*/
public static <T> T execute(String lockKey, Supplier<T> resultSupplier, long waitTime, TimeUnit unit,
ErrCodeEnum errCodeEnum) {
AssertUtils.assertTrue(StringUtils.isNotBlank(lockKey), ExceptionEnumType.PARAMETER_ILLEGALL);
boolean locked = false;
Lock lock = DistributedReentrantLock.newLock(lockKey);
try {
locked = waitTime > 0 ? lock.tryLock(waitTime, unit) : lock.tryLock();
} catch (InterruptedException e) {
throw new RuntimeException(String.format("lock error,lockResource:%s", lockKey), e);
}
if (errCodeEnum != null) {
AssertUtils.assertTrue(locked, errCodeEnum);
} else {
AssertUtils.assertTrue(locked, ExceptionEnumType.ACQUIRE_LOCK_FAIL);
}
try {
return resultSupplier.get();
} finally {
lock.unlock();
}
}
```
![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "点击并拖拽以移动")
幂等
==
在分布式系统设计中幂等性设计中十分重要的,尤其在复杂的微服务中一套系统中包含了多个子系统服务,而一个子系统服务往往会去调用另一个服务,而服务调用服务无非就是使用RPC通信或者restful,分布式系统中的网络延时或中断是避免不了的,通常会导致服务的调用层触发重试。具有这一性质的接口在设计时总是秉持这样的一种理念:调用接口发生异常并且重复尝试时,总是会造成系统所无法承受的损失,所以必须阻止这种现象的发生。
幂等通常会有两个维度:
1\. 空间维度上的幂等,即幂等对象的范围,是个人还是机构,是某一次交易还是某种类型的交易。
2\. 时间维度上的幂等,即幂等的保证时间,是几个小时、几天还是永久性的。
在实际系统中有很多操作,不管操作多少次,都应该产生一样的效果或返回相同的结果。以下这些应用场景也是通常比较常见的应用场景:
1\. 前端重复提交请求,且请求数据相同时,后台需要返回对应这个请求的相同结果。
2\. 发起一次支付请求,支付中心应该只扣用户账户一次钱,当遇到网络中断或系统异常时,也应该只扣一次钱。
3\. 发送消息,同样内容的短信发给用户只发一次。
4\. 创建业务订单,一次业务请求只能创建一个,重试请求创建多个就会出大问题。
5\. 基于msgId的消息幂等处理
在正式使用distributed-tools组件提供的幂等之前,我们先看下distributed-tools幂等组件的设计。
![1568168260336_1e02e9d6_5c23_4906_9488_9dc900ac5007](https://yqfile.alicdn.com/526041706bd1b5b773dee30c41dffbd16a2954b8.png)
* **幂等key提取能力:获取唯一幂等key**
幂等key的提取支持2中注解:IdempotentTxId、IdempotentTxIdGetter,任意方法添加以上2注解,即可提取到相关幂等key,前提条件是需要将Idempotent注解添加相关需要幂等的方法上。
如果单纯使用幂等模板进行业务处理,需要自己设置相关幂等key,且要保证其唯一性。
* **分布式锁服务能力:提供全局加锁、解锁的能力**
distributed-tools幂等组件需要使用自身提供的分布式锁功能,保证其并发唯一性,distributed-tools提供的分布式锁能够提供其可靠、稳定的加锁、解锁能力。
* **高性能的写入、查询能力:针对幂等结果查询与存储**
distributed-tools幂等组件提供了基于tair、redis的存储实现,同时支持自定义一级、二级存储通过spring依赖注入到IdempotentService,建议distributed-tools幂等存储结果一级存储tair mdb,二级存储ldb或者tablestore,一级存储保证其高性能,二级存储保证其可靠性。
二级存储并行查询会返回查询最快的幂等结果。
二级存储并行异步写入,进一步提高性能。
* **高可用的幂等写入、查询能力:幂等存储出现异常,不影响业务正常流程,增加容错**
distributed-tools幂等组件支持二级存储,为了保证其高可用,毕竟二级存储出现故障的概率太低,不会导致业务上不可用,如果二级存储同时出现故障,业务上做了一定的容错,针对不确定性的异常采取重试策略,会执行具体幂等方法。
一级存储与二级存储的写入与查询处理进行隔离,任何一级存储的异常不会影响整体业务执行。
在了解了distributed-tools组件幂等之后,接下来我们来看下如何去使用幂等组件,首先了解下common-api提供的幂等注解,具体幂等注解使用方式如下:
注解定义
使用范围
使用描述
Idempotent
方法
Idempotent需要定义到具体Method上。Idempotent有个属性定义:
expireDate表示幂等有效期,默认30天。
spelKey表示可以使用spring表达式生成幂等唯一ID,比如直接获取到对象属性或者方法或者其他表达式。
IdempotentTxId
参数、对象属性
IdempotentTxId可以直接定义到方法参数或者参数对象属性上,直接获取幂等ID
IdempotentTxIdGetter
方法
IdempotentTxIdGetter可以直接定义参数对象的方法上,调用该方法获取幂等ID
幂等拦截器获取幂等ID的优先级:
1. 首先判断Idempotent的spelKey的属性是否为空,如果不为空会根据spelKey定义的spring表达式生成幂等ID。
2. 其次判断参数是否包含IdempotentTxId注解,如果有IdempotentTxId,会直接获取参数值生成幂等ID。
3. 再次通过反射获取参数对象属性是否包含IdempotentTxId注解,如果对象属性包含IdempotentTxId注解会获取该参数对象属性生成幂等ID。
4. 最后以上三种情况仍未获取到幂等ID,会进一步通过反射获取参数对象的Method是否定义IdempotentTxIdGetter注解,如果包含该注解则通过反射生成幂等ID。
代码使用示例:
```
@Idempotent(spelKey = "#request.requestId", firstLevelExpireDate = 7,secondLevelExpireDate = 30)
public void execute(BizFlowRequest request) {
..................
}
```
![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "点击并拖拽以移动")
如上述代码表示从request获取requestId作为幂等key,一级存储有效期7天,二级存储有效期30天。
distributed-tools除了可以使用幂等注解外,幂等组件还提供了一个通用幂等模板IdempotentTemplate,使用幂等模板的前提必须设置tair.idempotent.enabled=true或者redis.idempotent.enabled=true,默认为false,同时需要指定幂等结果一级存储,幂等结果存储为可选项配置。
具体使用幂等模板IdempotentTemplate的方法如下:
```
/**
* 幂等模板处理器
*
* @param request 幂等Request信息
* @param executeSupplier 幂等处理回调function
* @param resultPreprocessConsumer 幂等结果回调function 可以对结果做些预处理
* @param ifResultNeedIdempotence 除了根据异常还需要根据结果判定是否需要幂等性的场景可以提供此参数
* @return
*/
public R execute(IdempotentRequest<P> request, Supplier<R> executeSupplier,
Consumer<IdempotentResult<P, R>> resultPreprocessConsumer, Predicate<R> ifResultNeedIdempotence) {
........
}
```
![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "点击并拖拽以移动")
**request:**
幂等参数IdempotentRequest组装,可以设置幂等参数和幂等唯一ID
**executeSupplier:**
具体幂等的方法逻辑,比如针对支付、下单接口,可以通过JDK8函数式接口Supplier Callback进行处理。
**resultBiConsumer:**
幂等返回结果的处理,该参数可以为空,如果为空采取默认的处理,根据幂等结果,如果成功、不可重试的异常错误码,直接返回结果,如果失败可重试异常错误码,会进行重试处理。
如果该参数值不为空,可以针对返回幂等结果进行特殊逻辑处理设置ResultStatus(ResultStatus包含三种状态包括成功、失败可重试、失败不可重试)。
[原文链接](https://yq.aliyun.com/articles/727048?utm_content=g_1000090492)
本文为云栖社区原创内容,未经允许不得转载。
相关推荐
化工行业点评报告:多因素催化炼化聚酯产业链,继续推荐“四大金刚+新凤鸣”.pdf
新版计算机专业课-四大金刚 33G计算机网络+操作系统+组成原理+数据结构课程 必备基础
在这个时代背景下,“四大金刚”成为了行业内的焦点,它们代表着通信行业的核心竞争力和未来发展趋势。 “四大金刚”通常指的是在通信行业中具有领先地位和技术优势的四家公司或业务板块。这些企业通常拥有强大的...
AS3.0四大金刚为ActionScript3.0 中开发应用程序提供了基础
通信:成长蓝筹大时代,聚焦“四大金刚”.pdf
治疗糖尿病的四大金刚穴位分别是胰俞、鱼际、太溪以及根据不同症状可能加入的内庭。这些穴位在中医理论中具有特定的功效,有助于调理体内阴阳平衡,改善糖尿病症状。 1. 胰俞穴:作为经外奇穴,胰俞对治疗糖尿病有...
Rust 实战小项目:游戏四大金刚 贪吃蛇、五子棋、俄罗斯方块、扫雷代码 Rust 作为一门高性能、内存安全的系统级编程语言,越来越受到开发者的青睐。为了深入学习和实践 Rust 语言,本项目实现了四个经典的游戏:贪吃蛇...
Rust 实战小项目:游戏四大金刚 贪吃蛇、五子棋、俄罗斯方块、扫雷源代码 Rust 作为一门高性能、内存安全的系统级编程语言,越来越受到开发者的青睐。为了深入学习和实践 Rust 语言,本项目实现了四个经典的游戏:...
以下是对“商务谈判的拒绝技巧中的‘四大金刚’”的详细解释: 1. 问题法: 问题法是一种巧妙的拒绝方式,它通过提出一系列相关问题,让对方意识到他们的要求可能过于苛刻。例如,在面对过高报价时,通过询问市场...
### Java异常处理的四大金刚:try、catch、finally和throw的深度解析 #### Java异常处理概述 异常处理机制是程序设计中的一个重要组成部分,它能够帮助程序员有效地管理程序运行过程中可能出现的各种错误情况。...
在AIOps的实施中,四大金刚的角色至关重要,它们分别是运维工程师、运维研发工程师、平台研发工程师和运维AI工程师。 运维工程师是AIOps的基础,他们负责识别运维痛点,定义问题域,并与AI相结合寻找解决方案。在...
计算机图形学是一门涵盖广泛领域的学科,主要研究如何在计算机中表示、处理和显示图形信息。在这个“计算机图形学绘制金刚石实验”中,我们将会深入探讨如何利用编程技术来模拟真实世界的物体——金刚石的视觉表现。...
Rust 作为一门高性能、内存安全的系统级编程语言,越来越受到开发者的青睐。为了深入学习和实践 Rust 语言,本项目实现了四个经典的游戏:贪吃蛇、五子棋、俄罗斯方块和扫雷。这些游戏不仅能够帮助开发者熟悉 Rust 语言...
《变形金刚Vista主题——打造个性化的科技桌面》 在数字化的世界里,个性化桌面成为了许多电脑用户表达自我风格的一种方式。"变形金刚Vista主题"就是这样一个独特的产品,它将热门电影《变形金刚》的元素融入到...
它包含了丰富的用户界面元素、文档视图架构、网络和数据库支持等,使得开发人员能够更方便地创建功能丰富的图形用户界面。在MFC中实现图形绘制,通常会使用CDC(Device Context)类,它代表设备上下文,是Windows...
1.定义二维坐标系原点位于屏幕中心,x轴水平向右为正,y轴铅直向上为正。 以二维坐标系原点为圆心绘制半径为r的圆,将圆的n等分点使用直线彼此连接形成金刚石图案。 2.程序运行界面提供“文件”、“绘图”和“帮助”...
"金刚1024控台使用说明书" 金刚1024控台是专业的灯光控制台,旨在提供简洁、易用的操作界面,帮助用户轻松控制灯光设备。以下是金刚1024控台的使用说明书提要: 一、安装 金刚1024控台的安装过程非常重要,需要...
加密金刚锁加密金刚锁加密金刚锁加密金刚锁加密金刚锁
金刚石散热片是一种高效能的热管理材料,尤其在微波射频领域有着广泛应用。金刚石因其独特的热学性质,成为室温下所有固体材料中最佳的导热体。这主要归因于其立方晶体结构,碳原子间的sp³共价键形成的强健网络,...
在编程领域,MFC(Microsoft Foundation Classes)是一个由微软提供的C++类库,它用于构建Windows应用程序。MFC提供了一种面向对象的方式来处理Windows API,使得开发者能够更方便地进行图形用户界面(GUI)的开发。...