点击上方“芋道源码”,选择“设为星标”
管她前浪,还是后浪?
能浪的浪,才是好浪!
每天 10:33 更新文章,每天掉亿点点头发...
源码精品专栏
来源:www.aneasystone.com/ archives/2020/08/spring-cloud- gateway-current-limiting.html
一、常见的限流场景
1.1 限流的对象
1.2 限流的处理方式
1.3 限流的架构
二、常见的限流算法
三、一些开源项目
3.1 Guava 的 RateLimiter
3.2 Bucket4j
3.3 Resilience4j
3.4 其他
四、在网关中实现限流
4.1 实现单机请求频率限流
4.2 实现分布式请求频率限流
4.3 实现单机并发量限流
总结
话说在 Spring Cloud Gateway 问世之前,Spring Cloud 的微服务世界里,网关一定非 Netflix Zuul 莫属。但是由于 Zuul 1.x 存在的一些问题,比如阻塞式的 API,不支持 WebSocket 等,一直被人所诟病,而且 Zuul 升级新版本依赖于 Netflix 公司,经过几次跳票之后,Spring 开源社区决定推出自己的网关组件,替代 Netflix Zuul。
从 18 年 6 月 Spring Cloud 发布的 Finchley 版本开始,Spring Cloud Gateway 逐渐崭露头角,它基于 Spring 5.0、Spring Boot 2.0 和 Project Reactor 等技术开发,不仅支持响应式和无阻塞式的 API,而且支持 WebSocket,和 Spring 框架紧密集成。尽管 Zuul 后来也推出了 2.x 版本,在底层使用了异步无阻塞式的 API,大大改善了其性能,但是目前看来 Spring 并没有打算继续集成它的计划。
根据官网的描述,Spring Cloud Gateway 的主要特性如下:
Built on Spring Framework 5, Project Reactor and Spring Boot 2.0
Able to match routes on any request attribute
Predicates and filters are specific to routes
Hystrix Circuit Breaker integration
Spring Cloud DiscoveryClient integration
Easy to write Predicates and Filters
Request Rate Limiting
Path Rewriting
可以看出 Spring Cloud Gateway 可以很方便的和 Spring Cloud 生态中的其他组件进行集成(比如:断路器和服务发现),而且提供了一套简单易写的 断言 (Predicates ,有的地方也翻译成 谓词 )和 过滤器 (Filters )机制,可以对每个 路由 (Routes )进行特殊请求处理。
最近在项目中使用了 Spring Cloud Gateway,并在它的基础上实现了一些高级特性,如限流和留痕,在网关的使用过程中遇到了不少的挑战,于是趁着项目结束,抽点时间系统地学习并总结下。这篇文章主要学习限流技术,首先我会介绍一些常见的限流场景和限流算法,然后介绍一些关于限流的开源项目,学习别人是如何实现限流的,最后介绍我是如何在网关中实现限流的,并分享一些实现过程中的经验和遇到的坑。
一、常见的限流场景
缓存 、降级 和 限流 被称为高并发、分布式系统的三驾马车,网关作为整个分布式系统中的第一道关卡,限流功能自然必不可少。通过限流,可以控制服务请求的速率,从而提高系统应对突发大流量的能力,让系统更具弹性。限流有着很多实际的应用场景,比如双十一的秒杀活动, 12306 的抢票等。
1.1 限流的对象
通过上面的介绍,我们对限流的概念可能感觉还是比较模糊,到底限流限的是什么?顾名思义,限流就是限制流量,但这里的流量是一个比较笼统的概念。如果考虑各种不同的场景,限流是非常复杂的,而且和具体的业务规则密切相关,可以考虑如下几种常见的场景:
限制某个接口一分钟内最多请求 100 次
限制某个用户的下载速度最多 100KB/S
限制某个用户同时只能对某个接口发起 5 路请求
限制某个 IP 来源禁止访问任何请求
从上面的例子可以看出,根据不同的请求者和请求资源,可以组合出不同的限流规则。可以根据请求者的 IP 来进行限流,或者根据请求对应的用户来限流,又或者根据某个特定的请求参数来限流。而限流的对象可以是请求的频率,传输的速率,或者并发量等,其中最常见的两个限流对象是请求频率和并发量,他们对应的限流被称为 请求频率限流 (Request rate limiting)和 并发量限流 (Concurrent requests limiting)。传输速率限流 在下载场景下比较常用,比如一些资源下载站会限制普通用户的下载速度,只有购买会员才能提速,这种限流的做法实际上和请求频率限流类似,只不过一个限制的是请求量的多少,一个限制的是请求数据报文的大小。这篇文章主要介绍请求频率限流和并发量限流。
1.2 限流的处理方式
在系统中设计限流方案时,有一个问题值得设计者去仔细考虑,当请求者被限流规则拦截之后,我们该如何返回结果。一般我们有下面三种限流的处理方式:
最简单的做法是拒绝服务,直接抛出异常,返回错误信息(比如返回 HTTP 状态码 429 Too Many Requests),或者给前端返回 302 重定向到一个错误页面,提示用户资源没有了或稍后再试。但是对于一些比较重要的接口不能直接拒绝,比如秒杀、下单等接口,我们既不希望用户请求太快,也不希望请求失败,这种情况一般会将请求放到一个消息队列中排队等待,消息队列可以起到削峰和限流的作用。第三种处理方式是服务降级,当触发限流条件时,直接返回兜底数据,比如查询商品库存的接口,可以默认返回有货。
1.3 限流的架构
针对不同的系统架构,需要使用不同的限流方案。如下图所示,服务部署的方式一般可以分为单机模式和集群模式:
单机模式的限流非常简单,可以直接基于内存就可以实现,而集群模式的限流必须依赖于某个“中心化”的组件,比如网关或 Redis,从而引出两种不同的限流架构:网关层限流 和 中间件限流 。
图片
网关作为整个分布式系统的入口,承担了所有的用户请求,所以在网关中进行限流是最合适不过的。网关层限流有时也被称为 接入层限流 。除了我们使用的 Spring Cloud Gateway,最常用的网关层组件还有 Nginx,可以通过它的 ngx_http_limit_req_module 模块,使用 limit_conn_zone、limit_req_zone、limit_rate 等指令很容易的实现并发量限流、请求频率限流和传输速率限流。这里不对 Nginx 作过多的说明,关于这几个指令的详细信息可以 参考 Nginx 的官方文档。
另一种限流架构是中间件限流 ,可以将限流的逻辑下沉到服务层。但是集群中的每个服务必须将自己的流量信息统一汇总到某个地方供其他服务读取,一般来说用 Redis 的比较多,Redis 提供的过期特性和 lua 脚本执行非常适合做限流。除了 Redis 这种中间件,还有很多类似的分布式缓存系统都可以使用,如 Hazelcast、Apache Ignite、Infinispan 等。
我们可以更进一步扩展上面的架构,将网关改为集群模式,虽然这还是网关层限流架构,但是由于网关变成了集群模式,所以网关必须依赖于中间件进行限流,这和上面讨论的中间件限流没有区别。
图片
推荐下自己做的 Spring Boot 的实战项目:
https://github.com/YunaiV/ruoyi-vue-pro
二、常见的限流算法
通过上面的学习,我们知道限流可以分为请求频率限流和并发量限流,根据系统架构的不同,又可以分为网关层限流和分布式限流。在不同的应用场景下,我们需要采用不同的限流算法。这一节将介绍一些主流的限流算法。
有一点要注意的是,利用池化技术也可以达到限流的目的,比如线程池或连接池,但这不是本文的重点。
2.1 固定窗口算法(Fixed Window)
固定窗口算法是一种最简单的限流算法,它根据限流的条件,将请求时间映射到一个时间窗口,再使用计数器累加访问次数。譬如限流条件为每分钟 5 次,那么就按照分钟为单位映射时间窗口,假设一个请求时间为 11:00:45,时间窗口就是 11:00:00 ~ 11:00:59,在这个时间窗口内设定一个计数器,每来一个请求计数器加一,当这个时间窗口的计数器超过 5 时,就触发限流条件。当请求时间落在下一个时间窗口内时(11:01:00 ~ 11:01:59),上一个窗口的计数器失效,当前的计数器清零,重新开始计数。
计数器算法非常容易实现,在单机场景下可以使用 AtomicLong、LongAdder 或 Semaphore 来实现计数,而在分布式场景下可以通过 Redis 的 INCR 和 EXPIRE 等命令并结合 EVAL 或 lua 脚本来实现,Redis 官网提供了几种简单的实现方式。无论是请求频率限流还是并发量限流都可以使用这个算法。
不过这个算法的缺陷也比较明显,那就是存在严重的临界问题。由于每过一个时间窗口,计数器就会清零,这使得限流效果不够平滑,恶意用户可以利用这个特点绕过我们的限流规则。如下图所示,我们的限流条件本来是每分钟 5 次,但是恶意用户在 11:00:00 ~ 11:00:59 这个时间窗口的后半分钟发起 5 次请求,接下来又在 11:01:00 ~ 11:01:59 这个时间窗口的前半分钟发起 5 次请求,这样我们的系统就在 1 分钟内承受了 10 次请求。
图片
2.2 滑动窗口算法(Rolling Window 或 Sliding Window)
为了解决固定窗口算法的临界问题,可以将时间窗口划分成更小的时间窗口,然后随着时间的滑动删除相应的小窗口,而不是直接滑过一个大窗口,这就是滑动窗口算法。我们为每个小时间窗口都设置一个计数器,大时间窗口的总请求次数就是每个小时间窗口的计数器的和。如下图所示,我们的时间窗口是 5 秒,可以按秒进行划分,将其划分成 5 个小窗口,时间每过一秒,时间窗口就滑过一秒:
图片
每次处理请求时,都需要计算所有小时间窗口的计数器的和,考虑到性能问题,划分的小时间窗口不宜过多,譬如限流条件是每小时 N 个,可以按分钟划分为 60 个窗口,而不是按秒划分成 3600 个。当然如果不考虑性能问题,划分粒度越细,限流效果就越平滑。相反,如果划分粒度越粗,限流效果就越不精确,出现临界问题的可能性也就越大,当划分粒度为 1 时,滑动窗口算法就退化成了固定窗口算法。由于这两种算法都使用了计数器,所以也被称为 计数器算法 (Counters)。
进一步思考我们发现,如果划分粒度最粗,也就是只有一个时间窗口时,滑动窗口算法退化成了固定窗口算法;那如果我们把划分粒度调到最细,又会如何呢?那么怎样才能让划分的时间窗口最细呢?时间窗口细到一定地步时,意味着每个时间窗口中只能容纳一个请求,这样我们可以省略计数器,只记录每个请求的时间,然后统计一段时间内的请求数有多少个即可。具体的实现可以参考Redis sorted set 技巧 和Sliding window log 算法。
2.3 漏桶算法(Leaky Bucket)
除了计数器算法,另一个很自然的限流思路是将所有的请求缓存到一个队列中,然后按某个固定的速度慢慢处理,这其实就是漏桶算法 (Leaky Bucket)。漏桶算法假设将请求装到一个桶中,桶的容量为 M,当桶满时,请求被丢弃。在桶的底部有一个洞,桶中的请求像水一样按固定的速度(每秒 r 个)漏出来。我们用下面这个形象的图来表示漏桶算法:
图片
桶的上面是个水龙头,我们的请求从水龙头流到桶中,水龙头流出的水速不定,有时快有时慢,这种忽快忽慢的流量叫做 Bursty flow 。如果桶中的水满了,多余的水就会溢出去,相当于请求被丢弃。从桶底部漏出的水速是固定不变的,可以看出漏桶算法可以平滑请求的速率。
漏桶算法可以通过一个队列来实现,如下图所示:
图片
当请求到达时,不直接处理请求,而是将其放入一个队列,然后另一个线程以固定的速率从队列中读取请求并处理,从而达到限流的目的。注意的是这个队列可以有不同的实现方式,比如设置请求的存活时间,或将队列改造成 PriorityQueue,根据请求的优先级排序而不是先进先出。当然队列也有满的时候,如果队列已经满了,那么请求只能被丢弃了。漏桶算法有一个缺陷,在处理突发流量时效率很低,于是人们又想出了下面的令牌桶算法。
2.4 令牌桶算法(Token Bucket)
令牌桶算法 (Token Bucket)是目前应用最广泛的一种限流算法,它的基本思想由两部分组成:生成令牌 和 消费令牌 。
令牌桶算法的图示如下:
图片
在上面的图中,我们将请求放在一个缓冲队列中,可以看出这一部分的逻辑和漏桶算法几乎一模一样,只不过在处理请求上,一个是以固定速率处理,一个是从桶中获取令牌后才处理。
仔细思考就会发现,令牌桶算法有一个很关键的问题,就是桶大小的设置,正是这个参数可以让令牌桶算法具备处理突发流量的能力。譬如将桶大小设置为 100,生成令牌的速度设置为每秒 10 个,那么在系统空闲一段时间的之后(桶中令牌一直没有消费,慢慢的会被装满),突然来了 50 个请求,这时系统可以直接按每秒 50 个的速度处理,随着桶中的令牌很快用完,处理速度又会慢慢降下来,和生成令牌速度趋于一致。这是令牌桶算法和漏桶算法最大的区别,漏桶算法无论来了多少请求,只会一直以每秒 10 个的速度进行处理。当然,处理突发流量虽然提高了系统性能,但也给系统带来了一定的压力,如果桶大小设置不合理,突发的大流量可能会直接压垮系统。
通过上面对令牌桶的原理分析,一般会有两种不同的实现方式。第一种方式是启动一个内部线程,不断的往桶中添加令牌,处理请求时从桶中获取令牌,和上面图中的处理逻辑一样。第二种方式不依赖于内部线程,而是在每次处理请求之前先实时计算出要填充的令牌数并填充,然后再从桶中获取令牌。下面是第二种方式的一种经典实现,其中 capacity 表示令牌桶大小,refillTokensPerOneMillis 表示填充速度,每毫秒填充多少个,availableTokens 表示令牌桶中还剩多少个令牌,lastRefillTimestamp 表示上一次填充时间。
public class TokenBucket {
private final long capacity;
private final double refillTokensPerOneMillis;
private double availableTokens;
private long lastRefillTimestamp;
public TokenBucket(long capacity, long refillTokens, long refillPeriodMillis) {
this.capacity = capacity;
this.refillTokensPerOneMillis = (double) refillTokens / (double) refillPeriodMillis;
this.availableTokens = capacity;
this.lastRefillTimestamp = System.currentTimeMillis();
}
synchronized public boolean tryConsume(int numberTokens) {
refill();
if (availableTokens < numberTokens) {
return false;
} else {
availableTokens -= numberTokens;
return true;
}
}
private void refill() {
long currentTimeMillis = System.currentTimeMillis();
if (currentTimeMillis > lastRefillTimestamp) {
long millisSinceLastRefill = currentTimeMillis - lastRefillTimestamp;
double refill = millisSinceLastRefill * refillTokensPerOneMillis;
this.availableTokens = Math.min(capacity, availableTokens + refill);
this.lastRefillTimestamp = currentTimeMillis;
}
}
}
可以像下面这样创建一个令牌桶(桶大小为 100,且每秒生成 100 个令牌):
TokenBucket limiter = new TokenBucket(100, 100, 1000);
从上面的代码片段可以看出,令牌桶算法的实现非常简单也非常高效,仅仅通过几个变量的运算就实现了完整的限流功能。核心逻辑在于 refill() 这个方法,在每次消费令牌时,计算当前时间和上一次填充的时间差,并根据填充速度计算出应该填充多少令牌。在重新填充令牌后,再判断请求的令牌数是否足够,如果不够,返回 false,如果足够,则减去令牌数,并返回 true。
在实际的应用中,往往不会直接使用这种原始的令牌桶算法,一般会在它的基础上作一些改进,比如,填充速率支持动态调整,令牌总数支持透支,基于 Redis 支持分布式限流等,不过总体来说还是符合令牌桶算法的整体框架,我们在后面学习一些开源项目时对此会有更深的体会。
推荐下自己做的 Spring Cloud 的实战项目:
https://github.com/YunaiV/onemall
三、一些开源项目
有很多开源项目中都实现了限流的功能,这一节通过一些开源项目的学习,了解限流是如何实现的。
3.1 Guava 的 RateLimiter
Google Guava 是一个强大的核心库,包含了很多有用的工具类,例如:集合、缓存、并发库、字符串处理、I/O 等等。其中在并发库中,Guava 提供了两个和限流相关的类:RateLimiter 和 SmoothRateLimiter。Guava 的 RateLimiter 基于令牌桶算法实现,不过在传统的令牌桶算法基础上做了点改进,支持两种不同的限流方式:平滑突发限流 (SmoothBursty) 和 平滑预热限流 (SmoothWarmingUp)。
下面的方法可以创建一个平滑突发限流器(SmoothBursty):
RateLimiter limiter = RateLimiter.create(5);
RateLimiter.create(5) 表示这个限流器容量为 5,并且每秒生成 5 个令牌,也就是每隔 200 毫秒生成一个。我们可以使用 limiter.acquire() 消费令牌,如果桶中令牌足够,返回 0,如果令牌不足,则阻塞等待,并返回等待的时间。我们连续请求几次:
System.out.println(limiter.acquire());
System.out.println(limiter.acquire());
System.out.println(limiter.acquire());
System.out.println(limiter.acquire());
输出结果如下:
0.0
0.198239
0.196083
0.200609
可以看出限流器创建之后,初始会有一个令牌,然后每隔 200 毫秒生成一个令牌,所以第一次请求直接返回 0,后面的请求都会阻塞大约 200 毫秒。另外,SmoothBursty 还具有应对突发的能力,而且 还允许消费未来的令牌,比如下面的例子:
RateLimiter limiter = RateLimiter.create(5);
System.out.println(limiter.acquire(10));
System.out.println(limiter.acquire(1));
System.out.println(limiter.acquire(1));
会得到类似下面的输出:
0.0
1.997428
0.192273
0.200616
限流器创建之后,初始令牌只有一个,但是我们请求 10 个令牌竟然也通过了,只不过看后面请求发现,第二次请求花了 2 秒左右的时间把前面的透支的令牌给补上了。
Guava 支持的另一种限流方式是平滑预热限流器(SmoothWarmingUp),可以通过下面的方法创建:
RateLimiter limiter = RateLimiter.create(2, 3, TimeUnit.SECONDS);
System.out.println(limiter.acquire(1));
System.out.println(limiter.acquire(1));
System.out.println(limiter.acquire(1));
System.out.println(limiter.acquire(1));
System.out.println(limiter.acquire(1));
第一个参数还是每秒创建的令牌数量,这里是每秒 2 个,也就是每 500 毫秒生成一个,后面的参数表示从冷启动速率过渡到平均速率的时间间隔,也就是所谓的热身时间间隔(warm up period)。我们看下输出结果:
0.0
1.329289
0.994375
0.662888
0.501287
第一个请求还是立即得到令牌,但是后面的请求和上面平滑突发限流就完全不一样了,按理来说 500 毫秒就会生成一个令牌,但是我们发现第二个请求却等了 1.3s,而不是 0.5s,后面第三个和第四个请求也等了一段时间。不过可以看出,等待时间在慢慢的接近 0.5s,直到第五个请求等待时间才开始变得正常。从第一个请求到第五个请求,这中间的时间间隔就是热身阶段,可以算出热身的时间就是我们设置的 3 秒。
3.2 Bucket4j
Bucket4j是一个基于令牌桶算法实现的强大的限流库,它不仅支持单机限流,还支持通过诸如 Hazelcast、Ignite、Coherence、Infinispan 或其他兼容 JCache API (JSR 107) 规范的分布式缓存实现分布式限流。
在使用 Bucket4j 之前,我们有必要先了解 Bucket4j 中的几个核心概念:
Bucket 接口代表了令牌桶的具体实现,也是我们操作的入口。它提供了诸如 tryConsume 和 tryConsumeAndReturnRemaining 这样的方法供我们消费令牌。可以通过下面的构造方法来创建Bucket:
Bucket bucket = Bucket4j.builder().addLimit(limit).build();
if(bucket.tryConsume(1)) {
System.out.println("ok");
} else {
System.out.println("error");
}
Bandwidth 的意思是带宽, 可以理解为限流的规则。Bucket4j 提供了两种方法来创建 Bandwidth:simple 和 classic。下面是 simple 方式创建的 Bandwidth,表示桶大小为 10,填充速度为每分钟 10 个令牌:
Bandwidth limit = Bandwidth.simple(10, Duration.ofMinutes(1));
simple方式桶大小和填充速度是一样的,classic 方式更灵活一点,可以自定义填充速度,下面的例子表示桶大小为 10,填充速度为每分钟 5 个令牌:
Refill filler = Refill.greedy(5, Duration.ofMinutes(1));
Bandwidth limit = Bandwidth.classic(10, filler);
其中,Refill 用于填充令牌桶,可以通过它定义填充速度,Bucket4j 有两种填充令牌的策略:间隔策略(intervally) 和 贪婪策略(greedy)。在上面的例子中我们使用的是贪婪策略,如果使用间隔策略可以像下面这样创建 Refill:
Refill filler = Refill.intervally(5, Duration.ofMinutes(1));
所谓间隔策略指的是每隔一段时间,一次性的填充所有令牌,比如上面的例子,会每隔一分钟,填充 5 个令牌,如下所示:
图片
而贪婪策略会尽可能贪婪的填充令牌,同样是上面的例子,会将一分钟划分成 5 个更小的时间单元,每隔 12 秒,填充 1 个令牌,如下所示:
图片
在了解了 Bucket4j 中的几个核心概念之后,我们再来看看官网介绍的一些特性:
Bucket4j 提供了丰富的文档,推荐在使用 Bucket4j 之前,先把官方文档中的 基本用法 和 高级特性 仔细阅读一遍。另外,关于 Bucket4j 的使用,推荐这篇文章 Rate limiting Spring MVC endpoints with bucket4j,这篇文章详细的讲解了如何在 Spring MVC 中使用拦截器和 Bucket4j 打造业务无侵入的限流方案,另外还讲解了如何使用 Hazelcast 实现分布式限流;另外,Rate Limiting a Spring API Using Bucket4j 这篇文章也是一份很好的入门教程,介绍了 Bucket4j 的基础知识,在文章的最后还提供了 Spring Boot Starter 的集成方式,结合 Spring Boot Actuator 很容易将限流指标集成到监控系统中。
和 Guava 的限流器相比,Bucket4j 的功能显然要更胜一筹,毕竟 Guava 的目的只是用作通用工具类,而不是用于限流的。使用 Bucket4j 基本上可以满足我们的大多数要求,不仅支持单机限流和分布式限流,而且可以很好的集成监控,搭配 Prometheus 和 Grafana 简直完美。值得一提的是,有很多开源项目譬如 JHipster API Gateway 就是使用 Bucket4j 来实现限流的。
Bucket4j 唯一不足的地方是它只支持请求频率限流,不支持并发量限流,另外还有一点,虽然 Bucket4j 支持分布式限流,但它是基于 Hazelcast 这样的分布式缓存系统实现的,不能使用 Redis,这在很多使用 Redis 作缓存的项目中就很不爽,所以我们还需要在开源的世界里继续探索。
3.3 Resilience4j
Resilience4j 是一款轻量级、易使用的高可用框架。用过 Spring Cloud 早期版本的同学肯定都听过 Netflix Hystrix,Resilience4j 的设计灵感就来自于它。自从 Hystrix 停止维护之后,官方也推荐大家使用 Resilience4j 来代替 Hystrix。
图片
Resilience4j 的底层采用 Vavr,这是一个非常轻量级的 Java 函数式库,使得 Resilience4j 非常适合函数式编程。Resilience4j 以装饰器模式提供对函数式接口或 lambda 表达式的封装,提供了一波高可用机制:重试(Retry) 、熔断(Circuit Breaker) 、限流(Rate Limiter) 、限时(Timer Limiter) 、隔离(Bulkhead) 、缓存(Caceh) 和 降级(Fallback) 。我们重点关注这里的两个功能:限流(Rate Limiter) 和 隔离(Bulkhead),Rate Limiter 是请求频率限流,Bulkhead 是并发量限流。
Resilience4j 提供了两种限流的实现:SemaphoreBasedRateLimiter 和 AtomicRateLimiter 。SemaphoreBasedRateLimiter 基于信号量实现,用户的每次请求都会申请一个信号量,并记录申请的时间,申请通过则允许请求,申请失败则限流,另外有一个内部线程会定期扫描过期的信号量并释放,很显然这是令牌桶的算法。AtomicRateLimiter 和上面的经典实现类似,不需要额外的线程,在处理每次请求时,根据距离上次请求的时间和生成令牌的速度自动填充。关于这二者的区别可以参考文章 Rate Limiter Internals in Resilience4j。
Resilience4j 也提供了两种隔离的实现:SemaphoreBulkhead 和 ThreadPoolBulkhead ,通过信号量或线程池控制请求的并发数,具体的用法参考官方文档,这里不再赘述。
下面是一个同时使用限流和隔离的例子:
// 创建一个 Bulkhead,最大并发量为 150
BulkheadConfig bulkheadConfig = BulkheadConfig.custom()
.maxConcurrentCalls(150)
.maxWaitTime(100)
.build();
Bulkhead bulkhead = Bulkhead.of("backendName", bulkheadConfig);
// 创建一个 RateLimiter,每秒允许一次请求
RateLimiterConfig rateLimiterConfig = RateLimiterConfig.custom()
.timeoutDuration(Duration.ofMillis(100))
.limitRefreshPeriod(Duration.ofSeconds(1))
.limitForPeriod(1)
.build();
RateLimiter rateLimiter = RateLimiter.of("backendName", rateLimiterConfig);
// 使用 Bulkhead 和 RateLimiter 装饰业务逻辑
Supplier supplier = () -> backendService.doSomething();
Supplier decoratedSupplier = Decorators.ofSupplier(supplier)
.withBulkhead(bulkhead)
.withRateLimiter(rateLimiter)
.decorate();
// 调用业务逻辑
Try try = Try.ofSupplier(decoratedSupplier);
assertThat(try.isSuccess()).isTrue();
Resilience4j 在功能特性上比 Bucket4j 强大不少,而且还支持并发量限流。不过最大的遗憾是,Resilience4j 不支持分布式限流。
3.4 其他
网上还有很多限流相关的开源项目,不可能一一介绍,这里列出来的只是冰山之一角:
https://github.com/mokies/ratelimitj
https://github.com/wangzheng0822/ratelimiter4j
https://github.com/wukq/rate-limiter
https://github.com/marcosbarbero/spring-cloud-zuul-ratelimit
https://github.com/onblog/SnowJena
https://gitee.com/zhanghaiyang/spring-boot-starter-current-limiting
https://github.com/Netflix/concurrency-limits
可以看出,限流技术在实际项目中应用非常广泛,大家对实现自己的限流算法乐此不疲,新算法和新实现层出不穷。但是找来找去,目前还没有找到一款开源项目完全满足我的需求。
我的需求其实很简单,需要同时满足两种不同的限流场景:请求频率限流和并发量限流,并且能同时满足两种不同的限流架构:单机限流和分布式限流。下面我们就开始在 Spring Cloud Gateway 中实现这几种限流,通过前面介绍的那些项目,我们取长补短,基本上都能用比较成熟的技术实现,只不过对于最后一种情况,分布式并发量限流,网上没有搜到现成的解决方案,在和同事讨论了几个晚上之后,想出一种新型的基于双窗口滑动的限流算法,我在这里抛砖引玉,欢迎大家批评指正,如果大家有更好的方法,也欢迎讨论。
四、在网关中实现限流
在文章一开始介绍 Spring Cloud Gateway 的特性时,我们注意到其中有一条 Request Rate Limiting,说明网关自带了限流的功能,但是 Spring Cloud Gateway 自带的限流有很多限制,譬如不支持单机限流,不支持并发量限流,而且它的请求频率限流也是不尽人意,这些都需要我们自己动手来解决。
4.1 实现单机请求频率限流
Spring Cloud Gateway 中定义了关于限流的一个接口 RateLimiter,如下:
public interface RateLimiter extends StatefulConfigurable {
Mono isAllowed(String routeId, String id);
}
这个接口就一个方法 isAllowed,第一个参数 routeId 表示请求路由的 ID,根据 routeId 可以获取限流相关的配置,第二个参数 id 表示要限流的对象的唯一标识,可以是用户名,也可以是 IP,或者其他的可以从 ServerWebExchange 中得到的信息。我们看下 RequestRateLimiterGatewayFilterFactory 中对 isAllowed 的调用逻辑:
@Override
public GatewayFilter apply(Config config) {
// 从配置中得到 KeyResolver
KeyResolver resolver = getOrDefault(config.keyResolver, defaultKeyResolver);
// 从配置中得到 RateLimiter
RateLimiter limiter = getOrDefault(config.rateLimiter,
defaultRateLimiter);
boolean denyEmpty = getOrDefault(config.denyEmptyKey, this.denyEmptyKey);
HttpStatusHolder emptyKeyStatus = HttpStatusHolder
.parse(getOrDefault(config.emptyKeyStatus, this.emptyKeyStatusCode));
return (exchange, chain) -> resolver.resolve(exchange).defaultIfEmpty(EMPTY_KEY)
.flatMap(key -> {
// 通过 KeyResolver 得到 key,作为唯一标识 id 传入 isAllowed() 方法
if (EMPTY_KEY.equals(key)) {
if (denyEmpty) {
setResponseStatus(exchange, emptyKeyStatus);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
// 获取当前路由 ID,作为 routeId 参数传入 isAllowed() 方法
String routeId = config.getRouteId();
if (routeId == null) {
Route route = exchange
.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
routeId = route.getId();
}
return limiter.isAllowed(routeId, key).flatMap(response -> {
for (Map.Entry header : response.getHeaders()
.entrySet()) {
exchange.getResponse().getHeaders().add(header.getKey(),
header.getValue());
}
// 请求允许,直接走到下一个 filter
if (response.isAllowed()) {
return chain.filter(exchange);
}
// 请求被限流,返回设置的 HTTP 状态码(默认是 429)
setResponseStatus(exchange, config.getStatusCode());
return exchange.getResponse().setComplete();
});
});
}
从上面的的逻辑可以看出,通过实现 KeyResolver 接口的 resolve 方法就可以自定义要限流的对象了。
public interface KeyResolver {
Mono resolve(ServerWebExchange exchange);
}
比如下面的 HostAddrKeyResolver 可以根据 IP 来限流:
public class HostAddrKeyResolver implements KeyResolver {
@Override
public Mono resolve(ServerWebExchange exchange) {
return Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
}
}
我们继续看 Spring Cloud Gateway 的代码发现,RateLimiter 接口只提供了一个实现类 RedisRateLimiter:
图片
很显然是基于 Redis 实现的限流,虽说通过 Redis 也可以实现单机限流,但是总感觉有些大材小用,而且对于那些没有 Redis 的环境很不友好。所以,我们要实现真正的本地限流。
我们从 Spring Cloud Gateway 的 pull request 中发现了一个新特性 Feature/local-rate-limiter,而且看提交记录,这个新特性很有可能会合并到 3.0.0 版本中。我们不妨来看下这个 local-rate-limiter 的实现:LocalRateLimiter.java,可以看出它是基于 Resilience4有意思的是,这个类 还有一个早期版本,是基于 Bucket4j 实现的:
public Mono isAllowed(String routeId, String id) {
Config routeConfig = loadConfiguration(routeId);
// How many requests per second do you want a user to be allowed to do?
int replenishRate = routeConfig.getReplenishRate();
// How many seconds for a token refresh?
int refreshPeriod = routeConfig.getRefreshPeriod();
// How many tokens are requested per request?
int requestedTokens = routeConfig.getRequestedTokens();
final io.github.resilience4j.ratelimiter.RateLimiter rateLimiter = RateLimiterRegistry
.ofDefaults()
.rateLimiter(id, createRateLimiterConfig(refreshPeriod, replenishRate));
final boolean allowed = rateLimiter.acquirePermission(requestedTokens);
final Long tokensLeft = (long) rateLimiter.getMetrics().getAvailablePermissions();
Response response = new Response(allowed, getHeaders(routeConfig, tokensLeft));
return Mono.just(response);
}
有意思的是,这个类 还有一个早期版本,是基于 Bucket4j 实现的:
public Mono isAllowed(String routeId, String id) {
Config routeConfig = loadConfiguration(routeId);
// How many requests per second do you want a user to be allowed to do?
int replenishRate = routeConfig.getReplenishRate();
// How much bursting do you want to allow?
int burstCapacity = routeConfig.getBurstCapacity();
// How many tokens are requested per request?
int requestedTokens = routeConfig.getRequestedTokens();
final Bucket bucket = bucketMap.computeIfAbsent(id,
(key) -> createBucket(replenishRate, burstCapacity));
final boolean allowed = bucket.tryConsume(requestedTokens);
Response response = new Response(allowed,
getHeaders(routeConfig, bucket.getAvailableTokens()));
return Mono.just(response);
}
实现方式都是类似的,在上面对 Bucket4j 和 Resilience4j 已经作了比较详细的介绍,这里不再赘述。不过从这里也可以看出 Spring 生态圈对 Resilience4j 是比较看好的,我们也可以将其引入到我们的项目中。
4.2 实现分布式请求频率限流
上面介绍了如何实现单机请求频率限流,接下来再看下分布式请求频率限流。这个就比较简单了,因为上面说了,Spring Cloud Gateway 自带了一个限流实现,就是 RedisRateLimiter,可以用于分布式限流。它的实现原理依然是基于令牌桶算法的,不过实现逻辑是放在一段 lua 脚本中的,我们可以在 src/main/resources/META-INF/scripts 目录下找到该脚本文件 request_rate_limiter.lua:
local tokens_key = KEYS[1]
local timestamp_key = KEYS[2]
local rate = tonumber(ARGV[1])
local capacity = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])
local fill_time = capacity/rate
local ttl = math.floor(fill_time*2)
local last_tokens = tonumber(redis.call("get", tokens_key))
if last_tokens == nil then
last_tokens = capacity
end
local last_refreshed = tonumber(redis.call("get", timestamp_key))
if last_refreshed == nil then
last_refreshed = 0
end
local delta = math.max(0, now-last_refreshed)
local filled_tokens = math.min(capacity, last_tokens+(delta*rate))
local allowed = filled_tokens >= requested
local new_tokens = filled_tokens
local allowed_num = 0
if allowed then
new_tokens = filled_tokens - requested
allowed_num = 1
end
if ttl > 0 then
redis.call("setex", tokens_key, ttl, new_tokens)
redis.call("setex", timestamp_key, ttl, now)
end
return { allowed_num, new_tokens }
这段代码和上面介绍令牌桶算法时用 Java 实现的那段经典代码几乎是一样的。这里使用 lua 脚本,主要是利用了 Redis 的单线程特性,以及执行 lua 脚本的原子性,避免了并发访问时可能出现请求量超出上限的现象。想象目前令牌桶中还剩 1 个令牌,此时有两个请求同时到来,判断令牌是否足够也是同时的,两个请求都认为还剩 1 个令牌,于是两个请求都被允许了。
有两种方式来配置 Spring Cloud Gateway 自带的限流。第一种方式是通过配置文件,比如下面所示的代码,可以对某个 route 进行限流:
spring:
cloud:
gateway:
routes:
- id: test
uri: http://httpbin.org:80/get
filters:
- name: RequestRateLimiter
args:
key-resolver: '#{@hostAddrKeyResolver}'
redis-rate-limiter.replenishRate: 1
redis-rate-limiter.burstCapacity: 3
其中,key-resolver 使用 SpEL 表达式 #{@beanName} 从 Spring 容器中获取 hostAddrKeyResolver 对象,burstCapacity 表示令牌桶的大小,replenishRate 表示每秒往桶中填充多少个令牌,也就是填充速度。
第二种方式是通过下面的代码来配置:
@Bean
public RouteLocator myRoutes(RouteLocatorBuilder builder) {
return builder.routes()
.route(p -> p
.path("/get")
.filters(filter -> filter.requestRateLimiter()
.rateLimiter(RedisRateLimiter.class, rl -> rl.setBurstCapacity(3).setReplenishRate(1)).and())
.uri("http://httpbin.org:80"))
.build();
}
这样就可以对某个 route 进行限流了。但是这里有一点要注意,Spring Cloud Gateway 自带的限流器有一个很大的坑,replenishRate 不支持设置小数,也就是说往桶中填充的 token 的速度最少为每秒 1 个,所以,如果我的限流规则是每分钟 10 个请求(按理说应该每 6 秒填充一次,或每秒填充 1/6 个 token),这种情况 Spring Cloud Gateway 就没法正确的限流。网上也有人提了 issue,support greater than a second resolution for the rate limiter,但还没有得到解决。
4.3 实现单机并发量限流
上面学习 Resilience4j 的时候,我们提到了 Resilience4j 的一个功能特性,叫 隔离(Bulkhead) 。Bulkhead 这个单词的意思是船的舱壁,利用舱壁可以将不同的船舱隔离起来,这样如果一个船舱破损进水,那么只损失这一个船舱,其它船舱可以不受影响。借鉴造船行业的经验,这种模式也被引入到软件行业,我们把它叫做 舱壁模式(Bulkhead pattern)。舱壁模式一般用于服务隔离,对于一些比较重要的系统资源,如 CPU、内存、连接数等,可以为每个服务设置各自的资源限制,防止某个异常的服务把系统的所有资源都消耗掉。这种服务隔离的思想同样可以用来做并发量限流。
正如前文所述,Resilience4j 提供了两种 Bulkhead 的实现:SemaphoreBulkhead 和 ThreadPoolBulkhead,这也正是舱壁模式常见的两种实现方案:一种是带计数的信号量,一种是固定大小的线程池。考虑到多线程场景下的线程切换成本,默认推荐使用信号量。
在操作系统基础课程中,我们学习过两个名词:互斥量(Mutex) 和 信号量(Semaphores) 。互斥量用于线程的互斥,它和临界区有点相似,只有拥有互斥对象的线程才有访问资源的权限,由于互斥对象只有一个,因此任何情况下只会有一个线程在访问此共享资源,从而保证了多线程可以安全的访问和操作共享资源。而信号量是用于线程的同步,这是由荷兰科学家 E.W.Dijkstra 提出的概念,它和互斥量不同,信号允许多个线程同时使用共享资源,但是它同时设定了访问共享资源的线程最大数目,从而可以进行并发量控制。
下面是使用信号量限制并发访问的一个简单例子:
public class SemaphoreTest {
private static ExecutorService threadPool = Executors.newFixedThreadPool(100);
private static Semaphore semaphore = new Semaphore(10);
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
threadPool.execute(new Runnable() {
@Override
public void run() {
try {
semaphore.acquire();
System.out.println("Request processing ...");
semaphore.release();
} catch (InterruptedException e) {
e.printStack();
}
}
});
}
threadPool.shutdown();
}
}
这里我们创建了 100 个线程同时执行,但是由于信号量计数为 10,所以同时只能有 10 个线程在处理请求。说到计数,实际上,在 Java 里除了 Semaphore 还有很多类也可以用作计数,比如 AtomicLong 或 LongAdder,这在并发量限流中非常常见,只是无法提供像信号量那样的阻塞能力:
public class AtomicLongTest {
private static ExecutorService threadPool = Executors.newFixedThreadPool(100);
private static AtomicLong atomic = new AtomicLong();
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
threadPool.execute(new Runnable() {
@Override
public void run() {
try {
if(atomic.incrementAndGet() > 10) {
System.out.println("Request rejected ...");
return;
}
System.out.println("Request processing ...");
atomic.decrementAndGet();
} catch (InterruptedException e) {
e.printStack();
}
}
});
}
threadPool.shutdown();
}
}
4.4 实现分布式并发量限流
通过在单机实现并发量限流,我们掌握了几种常用的手段:信号量、线程池、计数器,这些都是单机上的概念。那么稍微拓展下,如果能实现分布式信号量、分布式线程池、分布式计数器,那么实现分布式并发量限流不就易如反掌了吗?
关于分布式线程池,是我自己杜撰的词,在网上并没有找到类似的概念,比较接近的概念是资源调度和分发,但是又感觉不像,这里直接忽略吧。
关于分布式信号量,还真有这样的东西,比如 Apache Ignite 就提供了 IgniteSemaphore 用于创建分布式信号量,它的使用方式和 Semaphore 非常类似。使用 Redis 的 ZSet 也可以实现分布式信号量,比如 这篇博客介绍的方法,还有《Redis in Action》这本电子书中也提到了这样的例子,教你如何实现 Counting semaphores。另外,Redisson 也实现了基于 Redis 的分布式信号量 RSemaphore,用法也和 Semaphore 类似。使用分布式信号量可以很容易实现分布式并发量限流,实现方式和上面的单机并发量限流几乎是一样的。
最后,关于分布式计数器,实现方案也是多种多样。比如使用 Redis 的 INCR 就很容易实现,更有甚者,使用 MySQL 数据库也可以实现。只不过使用计数器要注意操作的原子性,每次请求时都要经过这三步操作:取计数器当前的值、判断是否超过阈值,超过则拒绝、将计数器的值自增。这其实和信号量的 P 操作是一样的,而释放就对应 V 操作。
所以,利用分布式信号量和计数器就可以实现并发量限流了吗?问题当然没有这么简单。实际上,上面通过信号量和计数器实现单机并发量限流的代码片段有一个严重 BUG:
semaphore.acquire();
System.out.println("Request processing ...");
semaphore.release();
想象一下如果在处理请求时出现异常了会怎么样?很显然,信号量被该线程获取了,但是却永远不会释放,如果请求异常多了,这将导致信号量被占满,最后一个请求也进不来。在单机场景下,这个问题可以很容易解决,加一个 finally 就行了:
try {
semaphore.acquire();
System.out.println("Request processing ...");
} catch (InterruptedException e) {
e.printStack();
} finally {
semaphore.release();
}
由于无论出现何种异常,finally 中的代码一定会执行,这样就保证了信号量一定会被释放。但是在分布式系统中,就不是加一个 finally 这么简单了。这是因为在分布式系统中可能存在的异常不一定是可被捕获的代码异常,还有可能是服务崩溃或者不可预知的系统宕机,就算是正常的服务重启也可能导致分布式信号量无法释放。
对于这个问题,我和几个同事连续讨论了几个晚上,想出了两种解决方法:第一种方法是使用带 TTL 的计数器,第二种方法是基于双窗口滑动的一种比较 tricky 的算法。
第一种方法比较容易理解,我们为每个请求赋予一个唯一 ID,并在 Redis 里写入一个键值对,key 为 requests_xxx(xxx 为请求 ID),value 为 1,并给这个 key 设置一个 TTL(如果你的应用中存在耗时非常长的请求,譬如对于一些 WebSockket 请求可能会持续几个小时,还需要开一个线程定期去刷新这个 key 的 TTL)。然后在判断并发量时,使用 KEYS 命令查询 requests_* 开头的 key 的个数,就可以知道当前一共有多少个请求,如果超过并发量上限则拒绝请求。这种方法可以很好的应对服务崩溃或重启的问题,由于每个 key 都设置了 TTL,所以经过一段时间后,这些 key 就会自动消失,就不会出现信号量占满不释放的情况了。但是这里使用 KEYS 命令查询请求个数是一个非常低效的做法,在请求量比较多的情况下,网关的性能会受到严重影响。我们可以把 KEYS 命令换成 SCAN,性能会得到些许提升,但总体来说效果还是很不理想的。
针对第一种方法,我们可以进一步优化,不用为每个请求写一个键值对,而是为每个分布式系统中的每个实例赋予一个唯一 ID,并在 Redis 里写一个键值对,key 为 instances_xxx(xxx 为实例 ID),value 为这个实例当前的并发量。同样的,我们为这个 key 设置一个 TTL,并且开启一个线程定期去刷新这个 TTL。每接受一个请求后,计数器加一,请求结束,计数器减一,这和单机场景下的处理方式一样,只不过在判断并发量时,还是需要使用 KEYS 或 SCAN 获取所有的实例,并计算出并发量的总和。不过由于实例个数是有限的,性能比之前的做法有了明显的提升。
第二种方法我称之为 双窗口滑动算法 ,结合了 TTL 计数器和滑动窗口算法。我们按分钟来设置一个时间窗口,在 Redis 里对应 202009051130 这样的一个 key,value 为计数器,表示请求的数量。当接受一个请求后,在当前的时间窗口中加一,当请求结束,在当前的时间窗口中减一,注意,接受请求和请求结束的时间窗口可能不是同一个。另外,我们还需要一个本地列表来记录当前实例正在处理的所有请求和请求对应的时间窗口,并通过一个小于时间窗口的定时线程(如 30 秒)来迁移过期的请求,所谓过期,指的是请求的时间窗口和当前时间窗口不一致。那么具体如何迁移呢?我们首先需要统计列表中一共有多少请求过期了,然后将列表中的过期请求时间更新为当前时间窗口,并从 Redis 中上一个时间窗口移动相应数量到当前时间窗口,也就是上一个时间窗口减 X,当前时间窗口加 X。由于迁移线程定期执行,所以过期的请求总是会被移动到当前窗口,最终 Redis 中只有当前时间窗口和上个时间窗口这两个时间窗口中有数据,再早一点的窗口时间中的数据会被往后迁移,所以可以给这个 key 设置一个 3 分钟或 5 分钟的 TTL。判断并发量时,由于只有两个 key,只需要使用 MGET 获取两个值相加即可。下面的流程图详细描述了算法的运行过程:
图片
其中有几个需要注意的细节:
请求结束时,直接在 Redis 中当前时间窗口减一即可,就算是负数也没关系。请求列表中的该请求不用急着删除,可以打上结束标记,在迁移线程中统一删除(当然,如果请求的开始时间和结束时间在同一个窗口,可以直接删除);
迁移的时间间隔要小于时间窗口,一般设置为 30s;
Redis 中的 key 一定要设置 TTL,时间至少为 2 个时间窗口,一般设置为 3 分钟;
迁移过程涉及到“从上一个时间窗口减”和“在当前时间窗口加”两个操作,要注意操作的原子性;
获取当前并发量可以通过 MGET 一次性读取两个时间窗口的值,不用 GET 两次;
获取并发量和判断并发量是否超限,这个过程也要注意操作的原子性。
总结
网关作为微服务架构中的重要一环,充当着一夫当关万夫莫开的角色,所以对网关服务的稳定性要求和性能要求都非常高。为保证网关服务的稳定性,一代又一代的程序员们前仆后继,想出了十八般武艺:限流、熔断、隔离、缓存、降级、等等等等。这篇文章从限流入手,详细介绍了限流的场景和算法,以及源码实现和可能踩到的坑。尽管限流只是网关的一个非常小的功能,但却影响到网关的方方面面,在系统架构的设计中至关重要。虽然我试着从不同的角度希望把限流介绍的更完全,但终究是管中窥豹,只见一斑,还有很多的内容没有介绍到,比如阿里开源的 Sentinel 组件也可以用于限流,因为篇幅有限未能展开。另外前文提到的 Netflix 不再维护 Hystrix 项目,这是因为他们把精力放到另一个限流项目 concurrency-limits 上了,这个项目的目标是打造一款自适应的,极具弹性的限流组件,它借鉴了 TCP 拥塞控制的算法(TCP congestion control algorithm),实现系统的自动限流,感兴趣的同学可以去它的项目主页了解更多内容。
本文篇幅较长,难免疏漏,如有问题,还望不吝赐教。
参考
微服务网关实战——Spring Cloud Gateway
《亿级流量网站架构核心技术》张开涛
聊聊高并发系统之限流特技
架构师成长之路之限流
微服务接口限流的设计与思考
常用4种限流算法介绍及比较
来谈谈限流-从概念到实现
高并发下的限流分析
计数器算法
基于Redis的限流系统的设计
API 调用次数限制实现
Techniques to Improve QoS
An alternative approach to rate limiting
Scaling your API with rate limiters
Brief overview of token-bucket algorithm
Rate limiting Spring MVC endpoints with bucket4j
Rate Limiter Internals in Resilience4j
高可用框架Resilience4j使用指南
阿里巴巴开源限流系统 Sentinel 全解析
spring cloud gateway 之限流篇
服务容错模式
你的API会自适应「弹性」限流吗?
- END -
欢迎加入我的知识星球,一起探讨架构,交流源码。加入方式,长按下方二维码噢 :
已在知识星球更新源码解析如下:
最近更新《芋道 SpringBoot 2.X 入门》系列,已经 101 余篇,覆盖了 MyBatis、Redis、MongoDB、ES、分库分表、读写分离、SpringMVC、Webflux、权限、WebSocket、Dubbo、RabbitMQ、RocketMQ、Kafka、性能测试等等内容。
提供近 3W 行代码的 SpringBoot 示例,以及超 4W 行代码的电商微服务项目。
获取方式:点“在看 ”,关注公众号并回复 666 领取,更多内容陆续奉上。
文章有帮助的话,在看,转发吧。
谢谢支持哟 (*^__^*)
你可能感兴趣的:(分布式,dbcp,编程语言,xhtml,java)
鸿蒙OS系统技术架构特性解析 - 探索新技术HarmonyOS
JfdCoding
harmonyos 架构 华为
随着智能设备的快速发展,操作系统的重要性日益凸显。鸿蒙OS(HarmonyOS)是华为公司为各类设备开发的全场景分布式操作系统,它具备强大的技术架构特性。本文将深入探讨鸿蒙OS的技术架构,并通过相应的源代码示例来解释其特性。分布式架构鸿蒙OS采用分布式架构,这是它最显著的特点之一。分布式架构允许不同设备之间实现高效的通信和资源共享,提供卓越的用户体验。下面是一个简单的代码示例,展示了如何在鸿蒙OS
[HarmonyOS]简单说一下鸿蒙架构
郝晨妤
HarmonyOS harmonyos 架构 华为 鸿蒙
鸿蒙操作系统(HarmonyOS)是由华为公司开发的一款面向全场景的操作分布式系统。它旨在提供一个统一的操作系统平台,支持多种设备,包括智能手机、平板电脑、智能电视、可穿戴设备、智能家居等。鸿蒙架构的设计目标是实现设备之间的无缝协同,提升用户体验。鸿蒙架构的主要特点1.分布式架构:分布式软总线:鸿蒙操作系统的核心技术之一,实现了设备之间的高效通信。通过分布式软总线,不同设备可以像同一设备上的不同进
JavaScript 类型转换的意外
神明木佑
javascript 开发语言 ecmascript
在JavaScript中,类型转换是将一个数据类型转换为另一个数据类型的过程。它可以是显式的,即通过使用特定的转换函数或操作符来实现,也可以是隐式的,即由JavaScript引擎自动完成。以下是JavaScript中的一些常见类型转换规则:字符串转换:使用String()函数或toString()方法可以将其他类型的值转换为字符串类型。varnum=42;varstr=String(num);//
【Kafka专栏 12】实时数据流与任务队列的较量 :Kafka与RabbitMQ有什么不同
夏之以寒
夏之以寒-kafka专栏 kafka rabbitmq 数据流 任务队列
作者名称:夏之以寒作者简介:专注于Java和大数据领域,致力于探索技术的边界,分享前沿的实践和洞见文章专栏:夏之以寒-kafka专栏专栏介绍:本专栏旨在以浅显易懂的方式介绍Kafka的基本概念、核心组件和使用场景,一步步构建起消息队列和流处理的知识体系,无论是对分布式系统感兴趣,还是准备在大数据领域迈出第一步,本专栏都提供所需的一切资源、指导,以及相关面试题,立刻免费订阅,开启Kafka学习之旅!
java设计模式单件模式_Head First设计模式(5):单件模式
weixin_39822493
java设计模式单件模式
更多的可以参考我的博客,也在陆续更新inghttp://www.hspweb.cn/单件模式确保一个类只有一个实例,并提供一个全局访点。例子:学生的学号生成方案,是在学生注册后,通过录入学生的基本信息,包括入学学年、学院、专业、班级等信息后,保存相应的资料后自动生成的。学号生成器的业务算法为:入学学年(2位)+学院代码(2位)+专业代码(2位)+班级代码(2位)+序号(2位)1.目录image2.
基于Java+Spring+vue+element实现旅游信息管理平台系统
网顺技术团队
成品程序项目 java spring vue.js spring boot 课程设计
基于Java+Spring+vue+element实现旅游信息管理平台系统作者主页网顺技术团队欢迎点赞收藏⭐留言文末获取源码联系方式查看下方微信号获取联系方式承接各种定制系统精彩系列推荐精彩专栏推荐订阅不然下次找不到哟Java毕设项目精品实战案例《1000套》感兴趣的可以先收藏起来,还有大家在毕设选题,项目以及论文编写等相关问题都可以给我留言咨询,希望帮助更多的人文章目录基于Java+Spring
JavaScript之BOM编程
qq_39095899
前端知识入门 javascript
BOM编程什么是BOM?BrowerObjectModel(浏览器对象模型,)关闭浏览器窗口、打开一个新的浏览器窗口、后退、前进、浏览器地址栏上的地址等,都是BOM编程BOM和DOM的区别与联系?BOM的顶级对象是:windowDOM的顶级对象是:document实际上BOM是包括DOM的!1、BOM编程中,window对象是顶级对象,代表浏览器窗口2、window有open和close方法,可以
JVM直接内存详解
fengdongnan
jvm 开发语言 java
直接内存学习JVM内存结构部分时遇到的最后一部分,直接内存。虽然和其他堆栈等不是核心部分,但其类似缓存的特点和与GC相关的特性显得有点特殊,比较好奇这个高速缓存有没有实际开发使用场景,所以写这篇博客记录直接内存的相关知识点与使用场景。概念直接内存(DirectMemory)是操作系统内存和Java内存共用的一片内存区域读写性能高,常见于NIO操作作为数据缓存区可以通过ByteBuffer.allo
Vue+Jest 单元测试
arron4210
前端 vue 单元测试 vue
新到一个公司,要求单元测试覆盖率达50%以上,我们都是后补的单测,其实单测的意义是根据需求提前写好,驱动开发,代替手动测试。然鹅这只是理想。。。这里总结一下各种遇到的单测场景挂载组件,调用elementui,mock函数```javascriptdescribe('页面验证',()=>{constwrapper=getVue({component:onlineFixedPrice,callback
聊聊这两年学习slam啃过的书!
3D视觉工坊
3D视觉从入门到精通 定位 编程语言 人工智能 机器学习 slam
入坑2年多,零七零八买了7、8本书,正好最近研一的新师弟让我来推荐几本,那么,独乐乐不如众乐乐,我就来巴拉巴拉一下我买的这些书吧。以下测评,仅代表个人观点,与书的作者无关(狗头保命)1、【C++PrimerPlus】嗯~这个灰常灰常厚的c++书是我买的第一本书,也是我所有书里除了java最厚的一本(java买了就没看),But,这本巨厚的c++我竟然翻完了!!!当年,年轻的我以为,看完这本书,我就
盘点时下最流行的十大编程语言优缺点,附2024年5月最新的编程语言排行榜单
嵌入式软件测试开发
IT杂谈 python 开发语言 c语言 c++ c# java javascript
文章目录前言一、Python二、C三、C++四、Java五、C#六、JavaScript七、VisualBasic八、Go九、SQL十、Fortran总结前言TIOBE公布了2024年5月最新的编程语言排行榜,本次的亮点是Fortran这个编程界的元老级语言,竟然在沉寂20多年后,再次闯入榜单的Top10。前10名分别是Python、C、C++、Java、C#、JavaScript、VisualB
【HeadFirst系列之HeadFirst设计模式】第5天之工厂模式:比萨店的秘密武器,轻松搞定对象创建!
工一木子
HeadFirst系列 HeadFirst设计模式 笔记 设计模式 工厂模式
工厂模式:比萨店的秘密武器,轻松搞定对象创建!大家好,今天我们来聊聊设计模式中的工厂模式。如果你曾经为对象的创建感到头疼,或者觉得代码中到处都是new关键字,那么工厂模式就是你的救星!本文基于《HeadFirst设计模式》的工厂模式章节,带你从比萨店的故事中轻松掌握工厂模式的精髓,附上Java代码示例,让你彻底理解并爱上它!1.简单工厂模式:比萨店的起步故事背景小明开了一家比萨店,刚开始只有两种比
HtML之JavaScript BOM编程
录大大i
前端 HTML JavaScript javascript html 前端
HtML之JavaScriptBOM编程windowhistory历史location地址栏document浏览器打开的.html文档consoleF12开发者工具的控制台screen屏幕navigator浏览器软件本身(历史原因一直沿用)sessionStorage会话级存储localStorage持久级存储window对象APIwindow对象的属性APIhistory窗口的访问历史locat
安全见闻1-9---清风
泷羽Sec-清风.春不晚
安全 网络安全
注:本文章源于泷羽SEC,如有侵权请联系我,违规必删学习请认准泷羽SEC学习视频:https://space.bilibili.com/350329294安全见闻1泷哥语录:安全领域什么都有,不要被表象所迷惑,无论技术也好还是其他方面也好,就是说学习之前,你得理解你要学的是什么?希望大家明白自己的渺小,知识的广博,时时刻刻保持平等的心。以后遇到问题要能够举一反三编程语言(安全学习重要排序)Pyth
结构型-代理模式(Proxy Pattern)
babstyt
设计模式 代理模式 java 设计模式 后端
什么是代理模式由于某些原因需要给某对象提供一个代理以控制对该对象的访问。这时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介。Java中的代理按照代理类生成时机不同又分为静态代理和动态代理。静态代理代理类在编译期就生成,而动态代理代理类则是在Java运行时动态生成。动态代理分为JDK代理和CGLib代理两种。结构抽象主题(Subject)类:通过接口或抽象类声明真
解析Python网络爬虫:核心技术、Scrapy框架、分布式爬虫(选择题、填空题、判断题)(第1、2、3、4、5、6、7、10、11章)
一口酪
python 爬虫 scrapy
第一章【填空题】网络爬虫又称网页蜘蛛或(网络机器人)网络爬虫能够按照一定的(规则),自动请求万维网站并提取网络数据。根据使用场景的不同,网络爬虫可分为(通用爬虫)和(聚焦爬虫)两种。爬虫可以爬取互联网上(公开)且可以访问到的网页信息。【判断题】爬虫是手动请求万维网网站且提取网页数据的程序。×爬虫爬取的是网站后台的数据。×通用爬虫用于将互联网上的网页下载到本地,形成一个互联网内容的镜像备份。√聚焦爬
【YashanDB 知识库】kettle 同步大表提示 java 内存溢出
数据库
【问题分类】数据导入导出【关键字】数据同步,kettle,数据迁移,java内存溢出【问题描述】kettle同步大表提示ERROR:couldnotcreatethejavavirtualmachine!【问题原因分析】java内存溢出【解决/规避方法】①增加JVM的堆内存大小。编辑Spoon.bat,增加堆大小到2GB,如:if"%PENTAHO_DI_JAVA_OPTIONS%"==""set
Dart 3.5更新对普通开发者有哪些影响?
哈喽,我是老刘Flutter3.24以及Dart3.5不久前发布了。突然觉得时间过得好快。六年前刚开始使用Flutter1.0的场景还在眼前。之前写了一篇文章盘点Flutter3.24的新功能对普通开发者有哪些影响。Flutter3.24对普通开发者有哪些影响?本文站在编程语言的角度,单独看看Dart3.5的新功能有哪些是我们普通开发者会用到的。Web平台和JS互操作性这个功能是为了将Flutte
JavaScript&ES6----数组去重的多种方法
方法一---双层for循环利用双层for循环,前一个循环前一项,后一个循环后一项,两两比对,如果发现重复的就用splice()属性,把重复的元素从数组arr中删除letarr=[2,5,1,5,3,2,'hello','1',4]letunique=(arr)=>{//第一层for循环循环数组前一项for(i=0;i{//声明一个新数组letnewArr=[];for(i=0;i{//声明一个新数
基于Selenium实现简单的任务流程操作
一个有女朋友的程序员
小工具 selenium java 责任链模式
文章目录概要技术介绍SeleniumWebDriverManager开始编码先导入对应的依赖初始化WebDriver建立流程链创建抽象节点初始化流程使用Selenium小结概要前段时间同事让我帮他老婆写一个可以自动操作浏览器办理业务的小程序,一开始是想着在网上找一找有没有的RPA软件(公司里用过金智维RPA,感觉自己比较熟悉),但是莫得找到,就只能自己用Java试一试了这里我选择Selenium来
Java文件加密
听风说起雨
android java
一、意义加密Java文件的主要目的是增加代码的安全性和保护知识产权。下面是一些加密Java文件的意义:防止代码泄露:加密Java文件可以减少源代码被非法访问、盗取或泄露的风险。特别是在开发商或个人希望保护其知识产权和商业机密时,加密可以有效防止未授权的访问。提高代码安全性:加密Java文件可以增加对恶意攻击的抵抗能力。通过加密,攻击者将难以理解和修改源代码,使得他们难以发现漏洞和进行攻击。防止反编
美团一面:说说synchronized的实现原理?
K&&K
面试 java
在Java中,synchronized是用于实现线程同步的关键字,其底层实现原理涉及对象头、监视器锁(Monitor)以及锁升级机制。以下是详细解析:1.对象头与MarkWord每个Java对象在内存中由三部分组成:对象头(Header):存储对象的元数据,包括锁状态、GC分代年龄等。实例数据(InstanceData):对象的成员变量。对齐填充(Padding):确保对象内存对齐。MarkWor
String...和String[]区别
码农张3
Java基础 java
publicstaticStringget(Stringurl,String...params){}类型后面三个点(String…),是从Java5开始,Java语言对方法参数支持一种新写法,叫可变长度参数列表,其语法就是类型后跟…,表示此处接受的参数为0到多个Object类型的对象,或者是一个Object[]。例如我们有一个方法叫做test(String…strings),那么你还可以写方法te
RSA加密解密
码农张3
Java基础 java 后端
packagecom.coder.common.utils.security;importorg.apache.commons.codec.binary.Base64;importjavax.crypto.Cipher;importjava.security.*;importjava.security.interfaces.RSAPrivateKey;importjava.security.int
40个JS常用使用技巧案例
javascript
大家好,我是V哥。在日常开发中,我们经常会使用JS解决页面的交互,在JS使用过程V哥总结了40个小技巧,分享给大家,废话不多说,马上开干。先赞再看后评论,腰缠万贯财进门。JS常用技巧案例以下是40个常用的JavaScript使用技巧,包含案例代码和解释:1.数组去重constarr=[1,2,2,3,4,4,5];constuniqueArr=[...newSet(arr)];console.lo
Vue中虚拟DOM的全面解析
七公子77
vue vue.js 前端 javascript
一、虚拟DOM的核心概念虚拟DOM(VirtualDOM)是一个轻量级的JavaScript对象,它是对真实DOM的抽象表示。在Vue中,组件模板会被编译成虚拟DOM树,通过Diff算法对比新旧虚拟DOM,计算出最小化的DOM操作,最终批量更新真实DOM。二、为什么需要虚拟DOM?1.直接操作DOM的问题性能瓶颈:DOM操作是浏览器中最昂贵的操作之一,频繁操作会导致性能下降。手动优化困难:开发者需
基于JAVA的象棋游戏的设计与实现
Python数据分析与机器学习
算法设计 java 青少年编程 c 开发语言 游戏 数据结构 算法
目录摘要第1章绪论1.1研究意义1.2研究目标第2章系统分析2.1相关技术和理论2.1.1开发环境2.1.2Java介绍2.1.3VSCode介绍2.2需求分析第3章设计与实现3.1程序流程图设计3.2游戏设计3.3棋盘棋子实现3.3.1基本数据结构——位棋盘3.3.2位棋盘的作用及初始化3.4功能实现3.4.1悔棋功能3.4.2认输3.5走棋和吃子规则实现3.6平台网络链接研究及实现第4章平台测
【C++】JsonCpp库
_清风过耳
C++ c++ 开发语言 1024程序员节 json
目录前置知识Json数据格式JsonCpp介绍安装jsoncpp库头文件使用介绍使用示例序列化反序列化前置知识Json数据格式Json数据格式是一种数据交换格式,采用完全独立于编程语言的文本格式存储和表示数据。例如:表示一个学生信息{"姓名":"xxx","年龄":18,"成绩":[88.5,99,58]}Json的数据类型包括对象,字符串,数字等对象:使用花括号{}括起来的表示一个对象数组:使用
Spring Boot项目Jar包加密详解
一休哥助手
java spring boot jar
目录引言Jar包加密的基础知识为什么需要加密Jar包Jar包加密的基本原理常用的Jar包加密工具ProGuardJavaguardJavaAgent
Python中的for循环语句详解
追逐程序梦想者
python 算法 前端 Python
在Python编程语言中,for循环语句是一种常用的控制结构,用于迭代遍历可迭代对象(如列表、元组、字符串等)中的元素。通过for循环,我们可以方便地对序列中的每个元素执行相同的操作,从而简化代码的编写和维护。本文将详细介绍Python中的for循环语句,并提供一些示例代码来帮助理解。for循环的语法结构如下所示:for变量in可迭代对象:#执行的代码块其中,关键字for标识了一个循环的开始,后面
java的(PO,VO,TO,BO,DAO,POJO)
Cb123456
VO TO BO POJO DAO
转:
http://www.cnblogs.com/yxnchinahlj/archive/2012/02/24/2366110.html
-------------------------------------------------------------------
O/R Mapping 是 Object Relational Mapping(对象关系映
spring ioc原理(看完后大家可以自己写一个spring)
aijuans
spring
最近,买了本Spring入门书:spring In Action 。大致浏览了下感觉还不错。就是入门了点。Manning的书还是不错的,我虽然不像哪些只看Manning书的人那样专注于Manning,但怀着崇敬 的心情和激情通览了一遍。又一次接受了IOC 、DI、AOP等Spring核心概念。 先就IOC和DI谈一点我的看法。IO
MyEclipse 2014中Customize Persperctive设置无效的解决方法
Kai_Ge
MyEclipse2014
高高兴兴下载个MyEclipse2014,发现工具条上多了个手机开发的按钮,心生不爽就想弄掉他!
结果发现Customize Persperctive失效!!
有说更新下就好了,可是国内Myeclipse访问不了,何谈更新...
so~这里提供了更新后的一下jar包,给大家使用!
1、将9个jar复制到myeclipse安装目录\plugins中
2、删除和这9个jar同包名但是版本号较
SpringMvc上传
120153216
springMVC
@RequestMapping(value = WebUrlConstant.UPLOADFILE)
@ResponseBody
public Map<String, Object> uploadFile(HttpServletRequest request,HttpServletResponse httpresponse) {
try {
//
Javascript----HTML DOM 事件
何必如此
JavaScript html Web
HTML DOM 事件允许Javascript在HTML文档元素中注册不同事件处理程序。
事件通常与函数结合使用,函数不会在事件发生前被执行!
注:DOM: 指明使用的 DOM 属性级别。
1.鼠标事件
属性
动态绑定和删除onclick事件
357029540
JavaScript jquery
因为对JQUERY和JS的动态绑定事件的不熟悉,今天花了好久的时间才把动态绑定和删除onclick事件搞定!现在分享下我的过程。
在我的查询页面,我将我的onclick事件绑定到了tr标签上同时传入当前行(this值)参数,这样可以在点击行上的任意地方时可以选中checkbox,但是在我的某一列上也有一个onclick事件是用于下载附件的,当
HttpClient|HttpClient请求详解
7454103
apache 应用服务器 网络协议 网络应用 Security
HttpClient 是 Apache Jakarta Common 下的子项目,可以用来提供高效的、最新的、功能丰富的支持 HTTP 协议的客户端编程工具包,并且它支持 HTTP 协议最新的版本和建议。本文首先介绍 HTTPClient,然后根据作者实际工作经验给出了一些常见问题的解决方法。HTTP 协议可能是现在 Internet 上使用得最多、最重要的协议了,越来越多的 Java 应用程序需
递归 逐层统计树形结构数据
darkranger
数据结构
将集合递归获取树形结构:
/**
*
* 递归获取数据
* @param alist:所有分类
* @param subjname:对应统计的项目名称
* @param pk:对应项目主键
* @param reportList: 最后统计的结果集
* @param count:项目级别
*/
public void getReportVO(Arr
访问WEB-INF下使用frameset标签页面出错的原因
aijuans
struts2
<frameset rows="61,*,24" cols="*" framespacing="0" frameborder="no" border="0">
MAVEN常用命令
avords
Maven库:
http://repo2.maven.org/maven2/
Maven依赖查询:
http://mvnrepository.com/
Maven常用命令: 1. 创建Maven的普通java项目: mvn archetype:create -DgroupId=packageName
PHP如果自带一个小型的web服务器就好了
houxinyou
apache 应用服务器 Web PHP 脚本
最近单位用PHP做网站,感觉PHP挺好的,不过有一些地方不太习惯,比如,环境搭建。PHP本身就是一个网站后台脚本,但用PHP做程序时还要下载apache,配置起来也不太很方便,虽然有好多配置好的apache+php+mysq的环境,但用起来总是心里不太舒服,因为我要的只是一个开发环境,如果是真实的运行环境,下个apahe也无所谓,但只是一个开发环境,总有一种杀鸡用牛刀的感觉。如果php自己的程序中
NoSQL数据库之Redis数据库管理(list类型)
bijian1013
redis 数据库 NoSQL
3.list类型及操作
List是一个链表结构,主要功能是push、pop、获取一个范围的所有值等等,操作key理解为链表的名字。Redis的list类型其实就是一个每个子元素都是string类型的双向链表。我们可以通过push、pop操作从链表的头部或者尾部添加删除元素,这样list既可以作为栈,又可以作为队列。
&nbs
谁在用Hadoop?
bingyingao
hadoop 数据挖掘 公司 应用场景
Hadoop技术的应用已经十分广泛了,而我是最近才开始对它有所了解,它在大数据领域的出色表现也让我产生了兴趣。浏览了他的官网,其中有一个页面专门介绍目前世界上有哪些公司在用Hadoop,这些公司涵盖各行各业,不乏一些大公司如alibaba,ebay,amazon,google,facebook,adobe等,主要用于日志分析、数据挖掘、机器学习、构建索引、业务报表等场景,这更加激发了学习它的热情。
【Spark七十六】Spark计算结果存到MySQL
bit1129
mysql
package spark.examples.db
import java.sql.{PreparedStatement, Connection, DriverManager}
import com.mysql.jdbc.Driver
import org.apache.spark.{SparkContext, SparkConf}
object SparkMySQLInteg
Scala: JVM上的函数编程
bookjovi
scala erlang haskell
说Scala是JVM上的函数编程一点也不为过,Scala把面向对象和函数型编程这两种主流编程范式结合了起来,对于熟悉各种编程范式的人而言Scala并没有带来太多革新的编程思想,scala主要的有点在于Java庞大的package优势,这样也就弥补了JVM平台上函数型编程的缺失,MS家.net上已经有了F#,JVM怎么能不跟上呢?
对本人而言
jar打成exe
bro_feng
java jar exe
今天要把jar包打成exe,jsmooth和exe4j都用了。
遇见几个问题。记录一下。
两个软件都很好使,网上都有图片教程,都挺不错。
首先肯定是要用自己的jre的,不然不能通用,其次别忘了把需要的lib放到classPath中。
困扰我很久的一个问题是,我自己打包成功后,在一个同事的没有装jdk的电脑上运行,就是不行,报错jvm.dll为无效的windows映像,如截图
最后发现
读《研磨设计模式》-代码笔记-策略模式-Strategy
bylijinnan
java 设计模式
声明: 本文只为方便我个人查阅和理解,详细的分析以及源代码请移步 原作者的博客http://chjavach.iteye.com/
/*
策略模式定义了一系列的算法,并将每一个算法封装起来,而且使它们还可以相互替换。策略模式让算法独立于使用它的客户而独立变化
简单理解:
1、将不同的策略提炼出一个共同接口。这是容易的,因为不同的策略,只是算法不同,需要传递的参数
cmd命令值cvfM命令
chenyu19891124
cmd
cmd命令还真是强大啊。今天发现jar -cvfM aa.rar @aaalist 就这行命令可以根据aaalist取出相应的文件
例如:
在d:\workspace\prpall\test.java 有这样一个文件,现在想要将这个文件打成一个包。运行如下命令即可比如在d:\wor
OpenJWeb(1.8) Java Web应用快速开发平台
comsci
java 框架 Web 项目管理 企业应用
OpenJWeb(1.8) Java Web应用快速开发平台的作者是我们技术联盟的成员,他最近推出了新版本的快速应用开发平台 OpenJWeb(1.8),我帮他做做宣传
OpenJWeb快速开发平台以快速开发为核心,整合先进的java 开源框架,本着自主开发+应用集成相结合的原则,旨在为政府、企事业单位、软件公司等平台用户提供一个架构透
Python 报错:IndentationError: unexpected indent
daizj
python tab 空格 缩进
IndentationError: unexpected indent 是缩进的问题,也有可能是tab和空格混用啦
Python开发者有意让违反了缩进规则的程序不能通过编译,以此来强制程序员养成良好的编程习惯。并且在Python语言里,缩进而非花括号或者某种关键字,被用于表示语句块的开始和退出。增加缩进表示语句块的开
HttpClient 超时设置
dongwei_6688
httpclient
HttpClient中的超时设置包含两个部分:
1. 建立连接超时,是指在httpclient客户端和服务器端建立连接过程中允许的最大等待时间
2. 读取数据超时,是指在建立连接后,等待读取服务器端的响应数据时允许的最大等待时间
在HttpClient 4.x中如下设置:
HttpClient httpclient = new DefaultHttpC
小鱼与波浪
dcj3sjt126com
一条小鱼游出水面看蓝天,偶然间遇到了波浪。 小鱼便与波浪在海面上游戏,随着波浪上下起伏、汹涌前进。 小鱼在波浪里兴奋得大叫:“你每天都过着这么刺激的生活吗?简直太棒了。” 波浪说:“岂只每天过这样的生活,几乎每一刻都这么刺激!还有更刺激的,要有潮汐变化,或者狂风暴雨,那才是兴奋得心脏都会跳出来。” 小鱼说:“真希望我也能变成一个波浪,每天随着风雨、潮汐流动,不知道有多么好!” 很快,小鱼
Error Code: 1175 You are using safe update mode and you tried to update a table
dcj3sjt126com
mysql
快速高效用:SET SQL_SAFE_UPDATES = 0;下面的就不要看了!
今日用MySQL Workbench进行数据库的管理更新时,执行一个更新的语句碰到以下错误提示:
Error Code: 1175
You are using safe update mode and you tried to update a table without a WHERE that
枚举类型详细介绍及方法定义
gaomysion
enum javaee
转发
http://developer.51cto.com/art/201107/275031.htm
枚举其实就是一种类型,跟int, char 这种差不多,就是定义变量时限制输入的,你只能够赋enum里面规定的值。建议大家可以看看,这两篇文章,《java枚举类型入门》和《C++的中的结构体和枚举》,供大家参考。
枚举类型是JDK5.0的新特征。Sun引进了一个全新的关键字enum
Merge Sorted Array
hcx2013
array
Given two sorted integer arrays nums1 and nums2, merge nums2 into nums1 as one sorted array.
Note:You may assume that nums1 has enough space (size that is
Expression Language 3.0新特性
jinnianshilongnian
el 3.0
Expression Language 3.0表达式语言规范最终版从2013-4-29发布到现在已经非常久的时间了;目前如Tomcat 8、Jetty 9、GlasshFish 4已经支持EL 3.0。新特性包括:如字符串拼接操作符、赋值、分号操作符、对象方法调用、Lambda表达式、静态字段/方法调用、构造器调用、Java8集合操作。目前Glassfish 4/Jetty实现最好,对大多数新特性
超越算法来看待个性化推荐
liyonghui160com
超越算法来看待个性化推荐
一提到个性化推荐,大家一般会想到协同过滤、文本相似等推荐算法,或是更高阶的模型推荐算法,百度的张栋说过,推荐40%取决于UI、30%取决于数据、20%取决于背景知识,虽然本人不是很认同这种比例,但推荐系统中,推荐算法起的作用起的作用是非常有限的。
就像任何
写给Javascript初学者的小小建议
pda158
JavaScript
一般初学JavaScript的时候最头痛的就是浏览器兼容问题。在Firefox下面好好的代码放到IE就不能显示了,又或者是在IE能正常显示的代码在firefox又报错了。 如果你正初学JavaScript并有着一样的处境的话建议你:初学JavaScript的时候无视DOM和BOM的兼容性,将更多的时间花在 了解语言本身(ECMAScript)。只在特定浏览器编写代码(Chrome/Fi
Java 枚举
ShihLei
java enum 枚举
注:文章内容大量借鉴使用网上的资料,可惜没有记录参考地址,只能再传对作者说声抱歉并表示感谢!
一 基础 1)语法
枚举类型只能有私有构造器(这样做可以保证客户代码没有办法新建一个enum的实例)
枚举实例必须最先定义
2)特性
&nb
Java SE 6 HotSpot虚拟机的垃圾回收机制
uuhorse
java HotSpot GC 垃圾回收 VM
官方资料,关于Java SE 6 HotSpot虚拟机的garbage Collection,非常全,英文。
http://www.oracle.com/technetwork/java/javase/gc-tuning-6-140523.html
Java SE 6 HotSpot[tm] Virtual Machine Garbage Collection Tuning
&