JSON Web Token - 在 Web 应用间安全地传递信息

转载自:http://blog.leapoahead.com/2015/09/06/understanding-jwt/

作者:John Wu

JSON Web Token(JWT)是一个非常轻巧的 规范。这个规范允许我们使用 JWT 在用户和服务器之间传递安全可靠的信息。

让我们来假想一下一个场景。在 A 用户关注了 B 用户的时候,系统发邮件给 B 用户,并且附有一个链接“点此关注 A 用户”。链接的地址可以是这样的

查看更多

分享到

Kong 集成 Jwt 插件

上一篇文章使用 Kong 完成了负载均衡的配置,本文介绍下在此基础上如何集成 jwt 插件来保护内部服务的安全。前置知识点:Jwt 基础概念。推荐阅读:

查看更多

分享到

初识 Kong 之负载均衡

使用 Kong Community Edition(社区版 v1.3.0)来搭建一个负载均衡器,由于 Kong 是基于 Openresty 的,而 Openresty 又是 Nginx 的二次封装,所有很多配置项和 Nginx 类似。

来看一个较为典型的 Nginx 负载均衡配置

查看更多

分享到

技术精进的三境界

最近更新了一篇 Docker 的文章,朋友跟我反馈说效果并不是很好,我回头看了下,的确没有我自己的特色,没有太多思考,让公众号显得有些「百货」了。经过反思,今后只在个人博客更新 Docker 相关的个人学习经验(传送门),个人公众号还是主要推送和 Java 结合较为紧密的内容。

查看更多

分享到

Docker Network—Bridge 模式

概述

Docker 强大的原因之一在于多个 Docker 容器之间的互相连接。涉及到连接,就引出了网络通信的几种模式。Docker 默认提供了 5 种网络驱动模式。

查看更多

分享到

JAVA 拾遗 --eqauls 和 hashCode 方法

缘起—lombok 引发的惨案

Lombok 是一种 Java™ 实用工具,可用于帮助开发人员消除 Java 的冗长,尤其是对于简单的 Java 对象(POJO)。它通过注解实现这一目的。

最近一个新项目中开始使用了 lombok,由于其真的是太简单易懂了,以至于我连文档都没看,直接就上手使用了,引发了一桩惨案。

查看更多

分享到

JAVA 拾遗 --Future 模式与 Promise 模式

写这篇文章的动机,是缘起于微信闲聊群的一场讨论,粗略整理下,主要涉及了以下几个具体的问题:

查看更多

分享到

离开魔都后的一点感想

看过我公众号简介的朋友会发现,我公众号的定位是分享一些个人的技术博客和杂谈,这其中的杂谈不仅包括了技术杂谈,也包含了自己的一些感悟。所以借此澄清,这是一篇杂文!总结了我在魔都这一年半不同角度的感悟。

技术沙龙

我前几天刚发过一个朋友圈:魔都得天独厚的优势便是各种技术沙龙,其中不乏有很多是免费的。我在魔都这些日子,参加了大概 13-15 场技术沙龙。参加这种技术分享会的动机很单纯,大家都是去学习互联网最前沿的技术的,但隐性的好处也很多:

  1. 你可以认识很多跟你同样有积极性的同行们,试想一下,这样一批放弃了周末休息时间,不睡懒觉,不打游戏,不陪妹子的人,必然是对技术充满了热情的一帮人。我有不少微信好友是在技术分享会上添加的。
  2. 有些朋友身处传统行业,平时的业务接触不到高并发,JVM 这些看似高大上的名词,但是技术沙龙带来了这样的可能性,让你没吃过猪肉,至少见过猪跑。映像比较深刻的是“美团点评技术团队”定期举办的技术沙龙,参加了两次,分享的很棒,而且大多数情况下,只需要在 APP 中报名即可参加。
  3. 同行间的技术方案分享。很有可能你正在负责公司某块业务的技术选型,而沙龙正好涉及了相关的内容,可以静静聆听下别人的解决方案,通过别人落地的架构,来指导自己的选型。这一点我也有例子真实的例子与之对应,大概是 17 年初,有一场 daocloud 举办的 springcloud 社区沙龙,由 DD 带来的 zuul 源码解读以及案例分享,记忆尤为深刻,当时我所在的公司也恰巧在对 zuul 进行改造,而 DD 分享的主题对于我的工作非常有帮助。

参加过不少沙龙,也踩过不少坑,我甚至误打误撞参加过两次“偏运维的技术分享”,权当作经历吧,总结参加的技术沙龙后,有以下几点建议:选择人数少的沙龙,越大的技术分享,讲师越想着受众面广,从而扼不住主题,会出现很多干货之外的内容;多举手提问,事实上我参加过的沙龙,我都会主动举手提问,并且是主持人刚问完“有没有观众有问题的”,我就立马举手,这时候大家都比较内敛,自己的命中率才会高。为什么要举手提问?期待讲师一句话把你的疑惑讲清楚是不现实的,但是这创造了你们后续交流的话题。分享结束后,主动加讲师的微信,进入深度交流阶段,这时候你便多了一个大牛好友。大牛好友用来干嘛?也不必指望为你解答疑惑,如果可以当然最好,偶尔看看他们的朋友圈都在分享些什么,至少能掌握最前沿的信息;有网红讲师参加的技术分享,尤其需要做好功课,刷几遍讲师最近的博客,提问时简明扼要的说出你提前准备了几天的问题,带上讲师出版的书索要签名,这样做的效果谁用谁知道。

[活动行] 是一款非常不错的 APP,再算上各个公众号(如 IT 大咖说),各种技术峰会(这个门票可能会有点贵,自费用户比较吃力),周末完全可以很充实。

博客 / 公众号与知识付费

这几个词放到一起说,作为一个博主 & 公众号作者,我接触到很多这个圈子的人,他们聪明,努力,乐于分享,而且往往是各自领域最厉害的那群人。以公众号粉丝数来评价,有近 10w 粉丝的大 V(在某个专业领域拥有这么多粉丝我认为已经算的上是 V 了,不同于鸡汤文作者受众广);以努力程度来评价,深夜 1-2 点还在桌边伏案的博主不在少数,就拿「芋艿源码」的博主「芋艿」来说吧,加班回家的地铁上笔记本一摊开就是一顿源码分析,年纪也老大不小了,保持着如同刚开始工作一般的热情,丝毫不给我们这些年轻人一口喘气的机会;还有一些博主,身处互联网公司,加班严重,依旧会挤出时间分享一线互联网的工作经验。身处这样一群人中间,时刻让你体会到什么叫「比你优秀的人还比你努力」。

介绍完我的所见,来聊聊我的所想,写作到底有什么意义。

一个直观的体验是成就感,当你的文章确切地帮助到了别人,获得了阅读量与赞,便会由衷地受到鼓舞,从而激发自己的创作欲。你会迸发出下一个目标,去帮助更多的人。收获自然也是会有的,靠公众号和博客积攒出的粉丝,成了那些知名博主的第一批付费粉丝。知名大牛在「极客时间」,「得到」等付费平台活跃,不愿意抛头露面的博主也有「知识星球」这样的平台和自己付费粉丝互动。知识付费是件好事,粉丝出了物力,赞助了自己认同的博主,交过钱后自然会更加用心;博主也有了挣钱的动力,一定会有激励作用,同时也会更加对自己的粉丝负责。未来这一块必然会越来越普及。

二来还是回到人脉、眼界这样的话题,当有了博主这个身份,被拉进一些社区原创作者交流群便成了顺利成章的事,在大佬云集的群里默默地看着讨论的话题,会提升自己的眼界。收到猎头的邮件也成了日常的习惯,来自 github 的招聘启事和来自博客的猎头,HR 流量可能会让你不堪其扰,那种感到愉悦的骚扰。「先生,游泳健身了解一下」下一份工作机会,可能就是来自于你的博客 & 公众号,毕竟这两个东西比你的简历更会说话。

魔都的生活

大四上初到上海寻找实习机会,直到今年 3 月份离开,算是有一年半了,对魔都这个陌生的城市也算了解了不少。年轻时不来魔都闯闯,后面迟早会有这个念头的,不如趁早体验下:高峰时期人潮拥挤的 1 号线,与日常乘坐的 9 号线末站,这都是魔都。跟同事去过人山人海的城隍庙,并没有太多惊喜,反倒是无意间逛到的思南路,仿佛给人穿越到欧洲小镇的错觉,惊喜。高楼林立下的田子坊,和远在厦门的鼓浪屿竟让我难以区分,这种相似只是那种一瞬间的感觉,最后留在脑海中的回忆还是在上海这种繁华的大都市竟然还有这样的净土。无论是浦东图书馆静谧的翻书声还是 MAO Livehouse 震颤到心脏的摇滚声,和同事们在外滩留下的足迹,联洋广场令人流连忘返的捞王… 魔都从不缺少物质享受。

说回魔都的工作,特指 IT 行业,也有了自己的一点认知,都说一线城市是北上广深,但说到底,上海的 IT/ 互联网并不是原以为的那样发达。美团点评,唯品会,饿了么,携程说大也大,还有金融领域的陆金所,蚂蚁金服(刚搬来)但总体来说肯定比不上北京,但这丝毫阻挡不了江浙沪有志青年的热情,即使有房价这个负面光环加护。

上海的压力来自于各个方面:孤独,消费,房价,工作额度,任意一个方面都会击垮一个人,再加上互联网的人来人往,都让我感受到上海这座现代化都市背后的冷漠。初到南京,HR 问我为什么不在上海发展,我竟答不出来。是啊,普遍都认为上海机遇更多,但是,机遇的定义没人说的清,正如离开上海的原因说不清一般。值得留恋的景,值得留恋的机遇,值得留恋的人,都带不走。

应届生的境遇

到了 18 年的春招,似乎已经挂不上「应届生」的名号了。时隔毕业半年多,同学们的工作也都稳定了下来,校园时光真的只存在于念想中了。曾经的自己仰仗着自己的应届光环,如今办理入职后才发现,自己已经是往届生了,有什么区别?心境真的会变,我自觉认识到再也不能仗着应届那么有恃无恐了。现在的职场上必须得拿能力和那些工作比你早几年接触工作的人去竞争了,应聘时再也不会说:我是来贵司学习的。公司需要为之创造价值的人,说来不怕矫情,以上这种念头最近在脑海中酝酿了很久。

与应届生相对的是那些工作十几年的老人,为什么放到这儿一起说,主要是在魔都见识到了一些年纪较大的应聘者,简历空有十年工作经验,实际怕是十个一年经验。不知道是否只有我会出现这种情况,刚毕业时知识储备不足每天下班坚持看书,看文章都不觉累,反倒是工作半年后有了熟练度了,这股学习的激情会下降。这点我尤其佩服我原来的老大和朋友圈一些年纪较大的程序员,几年如一日地对技术痴迷。警惕成为一个重复工作的码农。

学习技巧

编程技术如何精进?我秉承自上而下的顺序来谈下自己的思考,仅供各位参考。

  1. 业务驱动型。编程是为了解决问题,如果你的程序应用于生产中,这是最锻炼编码水平,无论是导入一个超大的 excel 这种小 case,还是抗住千万级别并发的电商系统,伴随着业务问题而进行的 coding 最有意义。反观我交接的最后一个星期,干不了什么活,都是些零碎琐事,完全提炼不出什么精髓用于写作。如果你的系统日活只有几百,考虑百万级别的并发压力实属是杞人忧天,还是优先把项目上线了更为实在。
  2. 源码阅读型。talk is cheap,show me the code。大多数设计精良的框架源码都伴随着巧妙的设计和丰富的注释,这些是学习编码技巧的绝佳途径,代码不会说谎,他就放在那儿,关键就在于你愿不愿点进去,去分析他。很多人拒绝阅读源码,自认为水平不够,其实实在是妄自菲薄,很多优秀的源码(如 spring)不仅可以提升你对框架工作原理的理解,也有助于自身编码的规范。
  3. 文档型。很多技术博客文章来源于这项技术的官方文档,很多小白喜欢阅读博客,殊不知博客已经是别人二次消化后的产物,存在不少理解的缺失。再者,较为前沿的技术,第一手博客还没出产,只有官方文档,比如 springboot 2.0,其实官方文档已经很细致入微地介绍了各种特性,完全没必要等待别人的翻译。学习一门新的技术,我通常的做法是:博客了解其大概用途,紧接着直接阅读官方文档,同时也避免了版本滞后性等问题。
  4. 博客型、搜索引擎型。这两者放到最下面,并不是排斥它们,也不是在否认 google 和 Stack Overflow 的伟大。而是想提出这样的观点:博客和搜索引擎适用于解决问题,从宏观的了解一项技术。博客和搜索引擎并不向你保证其准确性,他只告诉这样做或许有效,这样做或许正确。但是好处也很明显,经过了博主的梳理,可以更好的帮助读者建立知识体系,毕竟我自己也是写博客的 [微笑 face],但不要忘了有空去翻一翻 2,3 两点

夜深了,先写这么多吧。

分享到

深入理解 RPC 之集群篇

上一篇文章分析了服务的注册与发现,这一篇文章着重分析下 RPC 框架都会用到的集群的相关知识。

集群 (Cluster) 本身并不具备太多知识点,在分布式系统中,集群一般涵盖了负载均衡(LoadBalance),高可用(HA),路由(Route)等等概念,每个 RPC 框架对集群支持的程度不同,本文着重分析前两者 – 负载均衡和高可用。

集群概述

在此之前的《深入理解 RPC》系列文章,对 RPC 的分析着重还是放在服务之间的点对点调用,而分布式服务中每个服务必然不止一个实例,不同服务的实例和相同服务的多个实例构成了一个错综复杂的分布式环境,在服务治理框架中正是借助了 Cluster 这一层来应对这一难题。还是以博主较为熟悉的 motan 这个框架来介绍 Cluster 的作用。

先来看看 Cluster 的顶层接口:

1
2
3
4
5
6
7
8
9
10
11
@Spi(scope = Scope.PROTOTYPE)
public interface Cluster<T> extends Caller<T> {
@Override
void init();
void setUrl(URL url);
void setLoadBalance(LoadBalance<T> loadBalance);//<1>
void setHaStrategy(HaStrategy<T> haStrategy);//<2>
void onRefresh(List<Referer<T>> referers);
List<Referer<T>> getReferers();
LoadBalance<T> getLoadBalance();
}

在概述中,我们只关心 Cluster 接口中的两个方法,它揭示了 Cluster 在服务治理中的地位

<1> 指定负载均衡算法

<2> 指定高可用策略(容错机制)

https://kirito.iocoder.cn/TIM%E5%9B%BE%E7%89%8720180227151838.png

我们需要对所谓的负载均衡策略和高可用策略有一定的理解,才能够搞清楚集群是如何运作的。

负载均衡

说到负载均衡,大多数人可能立刻联想到了 nginx。负载均衡可以分为服务端负载均衡和客户端负载均衡,而服务端负载均衡又按照实现方式的不同可以划分为软件负载均衡和硬件负载均衡,nginx 便是典型的软件负载均衡。而我们今天所要介绍的 RPC 中的负载均衡则主要是客户端负载均衡。如何区分也很简单,用笔者自己的话来描述下

在 RPC 调用中,客户端持有所有的服务端节点引用,自行通过负载均衡算法选择一个节点进行访问,这便是客户端负载均衡。

客户端如何获取到所有的服务端节点引用呢?一般是通过配置的方式,或者是从上一篇文章介绍的服务注册与发现组件中获取。

负载均衡接口分析

motan 中的负载均衡抽象:

1
2
3
4
5
6
7
@Spi(scope = Scope.PROTOTYPE)
public interface LoadBalance<T> {
void onRefresh(List<Referer<T>> referers);
Referer<T> select(Request request);//<1>
void selectToHolder(Request request, List<Referer<T>> refersHolder);
void setWeightString(String weightString);
}

ribbon 中的负载均衡抽象:

1
2
3
4
5
public interface IRule{
public Server choose(Object key);//<1>
public void setLoadBalancer(ILoadBalancer lb);
public ILoadBalancer getLoadBalancer();
}

<1> 对比下两个 RPC 框架对负载均衡的抽象可以发现,其实负载均衡策略干的事很简单,就是根据请求返回一个服务节点。在 motan 中对服务端的点对点调用抽象成了 Referer,而在 ribbon 中则是 Server。

几种负载均衡算法

负载均衡算法有几种经典实现,已经是老生常谈了,总结后主要有如下几个:

  1. 轮询(Round Robin)
  2. 加权轮询(Weight Round Robin)
  3. 随机(Random)
  4. 加权随机(Weight Random)
  5. 源地址哈希(Hash)
  6. 一致性哈希(ConsistentHash)
  7. 最小连接数(Least Connections)
  8. 低并发优先(Active Weight)

每个框架支持的实现都不太一样,如 ribbon 支持的负载均衡策略

策略名 策略描述 实现说明
BestAvailableRule 选择一个最小并发请求的 server 逐个考察 Server,如果 Server 被 tripped 了,则忽略,在选择其中 ActiveRequestsCount 最小的 server
AvailabilityFilteringRule 过滤掉那些因为一直连接失败的被标记为 circuit tripped 的后端 server,并过滤掉那些高并发的的后端 server(active connections 超过配置的阈值) 使用一个 AvailabilityPredicate 来包含过滤 server 的逻辑,其实就就是检查 status 里记录的各个 server 的运行状态
WeightedResponseTimeRule 根据响应时间分配一个 weight,响应时间越长,weight 越小,被选中的可能性越低。 一个后台线程定期的从 status 里面读取评价响应时间,为每个 server 计算一个 weight。Weight 的计算也比较简单 responsetime 减去每个 server 自己平均的 responsetime 是 server 的权重。当刚开始运行,没有形成 status 时,使用 RoundRobinRule 策略选择 server。
RetryRule 对选定的负载均衡策略机上重试机制。 在一个配置时间段内当选择 server 不成功,则一直尝试使用 subRule 的方式选择一个可用的 server
RoundRobinRule roundRobin 方式轮询选择 server 轮询 index,选择 index 对应位置的 server
RandomRule 随机选择一个 server 在 index 上随机,选择 index 对应位置的 server
ZoneAvoidanceRule 复合判断 server 所在区域的性能和 server 的可用性选择 server 使用 ZoneAvoidancePredicate 和 AvailabilityPredicate 来判断是否选择某个 server,前一个判断判定一个 zone 的运行性能是否可用,剔除不可用的 zone(的所有 server),AvailabilityPredicate 用于过滤掉连接数过多的 Server。

motan 支持的负载均衡策略

策略名 策略描述
Random 随机选择一个 server
RoundRobin roundRobin 方式轮询选择 server
ConsistentHash 一致性 Hash,保证同一源地址的请求落到同一个服务端,能够应对服务端机器的动态上下线 (实际上并没有严格做到一致性 hash,motan 的实现只能满足粘滞 hash,只保证 server 节点变更周期内相同对请求落在相同的 server 上,比较适合用在二级缓存场景)
LocalFirst 当 server 列表中包含本地暴露的可用服务时,优先使用此服务。否则使用低并发优先 ActiveWeight 负载均衡策略
ActiveWeight 并发量越小的 server,优先级越高
ConfigurableWeight 加权随机

算法很多,有些负载均衡算法的实现复杂度也很高,请教了一些朋友,发现用的最多还是 RoundRobin,Random 这两种。可能和他们实现起来很简单有关,很多运用到 RPC 框架的项目也都是保持了默认配置。

而这两种经典复杂均衡算法实现起来是很简单的,在此给出网上的简易实现,方便大家更直观的了解。

服务列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class IpMap
{
// 待路由的 Ip 列表,Key 代表 Ip,Value 代表该 Ip 的权重
public static HashMap<String, Integer> serverWeightMap =
new HashMap<String, Integer>();
static
{
serverWeightMap.put("192.168.1.100", 1);
serverWeightMap.put("192.168.1.101", 1);
// 权重为 4
serverWeightMap.put("192.168.1.102", 4);
serverWeightMap.put("192.168.1.103", 1);
serverWeightMap.put("192.168.1.104", 1);
// 权重为 3
serverWeightMap.put("192.168.1.105", 3);
serverWeightMap.put("192.168.1.106", 1);
// 权重为 2
serverWeightMap.put("192.168.1.107", 2);
serverWeightMap.put("192.168.1.108", 1);
serverWeightMap.put("192.168.1.109", 1);
serverWeightMap.put("192.168.1.110", 1);
}
}

轮询(Round Robin)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class RoundRobin
{
private static Integer pos = 0;

public static String getServer()
{
// 重建一个 Map,避免服务器的上下线导致的并发问题
Map<String, Integer> serverMap =
new HashMap<String, Integer>();
serverMap.putAll(IpMap.serverWeightMap);

// 取得 Ip 地址 List
Set<String> keySet = serverMap.keySet();
ArrayList<String> keyList = new ArrayList<String>();
keyList.addAll(keySet);

String server = null;
synchronized (pos)
{
if (pos > keySet.size())
pos = 0;
server = keyList.get(pos);
pos ++;
}

return server;
}
}

随机(Random)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Random
{
public static String getServer()
{
// 重建一个 Map,避免服务器的上下线导致的并发问题
Map<String, Integer> serverMap =
new HashMap<String, Integer>();
serverMap.putAll(IpMap.serverWeightMap);

// 取得 Ip 地址 List
Set<String> keySet = serverMap.keySet();
ArrayList<String> keyList = new ArrayList<String>();
keyList.addAll(keySet);

java.util.Random random = new java.util.Random();
int randomPos = random.nextInt(keyList.size());

return keyList.get(randomPos);
}
}

高可用策略

高可用(HA)策略一般也被称作容错机制,分布式系统中出错是常态,但服务却不能停止响应,6 个 9 一直是各个公司的努力方向。当一次请求失败之后,是重试呢?还是继续请求其他机器?抑或是记录下这次失败?下面是集群中的几种常用高可用策略:

  1. 失效转移(failover)

    当出现失败,重试其他服务器,通常用于读操作等幂等行为,重试会带来更长延迟。该高可用策略会受到负载均衡算法的限制,比如失效转移强调需要重试其他机器,但一致性 Hash 这类负载均衡算法便会与其存在冲突(个人认为一致性 Hash 在 RPC 的客户端负载均衡中意义不是很大)

  2. 快速失败(failfast)

    只发起一次调用,失败立即报错,通常用于非幂等性的写操作。

    如果在 motan,dubbo 等配置中设置了重试次数 >0,又配置了该高可用策略,则重试效果也不会生效,由此可见集群中的各个配置可能是会相互影响的。

  3. 失效安全(failsafe)

    出现异常时忽略,但记录这一次失败,存入日志中。

  4. 失效自动恢复(failback)

    后台记录失败请求,定时重发。通常用于消息通知操作。

  5. 并行调用(forking)

    只要一个成功即返回,通常用于实时性要求较高的读操作。需要牺牲一定的服务资源。

  6. 广播(broadcast)

    广播调用,所有提供逐个调用,任意一台报错则报错。通常用于更新提供方本地状态,速度慢,任意一台报错则报错。

高可用接口分析

以 motan 的 HaStrategy 为例来介绍高可用在集群中的实现细节

1
2
3
4
5
@Spi(scope = Scope.PROTOTYPE)
public interface HaStrategy<T> {
void setUrl(URL url);
Response call(Request request, LoadBalance<T> loadBalance);//<1>
}

<1> 如我之前所述,高可用策略依赖于请求和一个特定的负载均衡算法,返回一个响应。

快速失败(failfast)

1
2
3
4
5
6
7
8
9
@SpiMeta(name = "failfast")
public class FailfastHaStrategy<T> extends AbstractHaStrategy<T> {

@Override
public Response call(Request request, LoadBalance<T> loadBalance) {
Referer<T> refer = loadBalance.select(request);
return refer.call(request);
}
}

motan 实现了两个高可用策略,其一便是 failfast,非常简单,只进行一次负载均衡节点的选取,接着发起点对点的调用。

失效转移(failover)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
@SpiMeta(name = "failover")
public class FailoverHaStrategy<T> extends AbstractHaStrategy<T> {

protected ThreadLocal<List<Referer<T>>> referersHolder = new ThreadLocal<List<Referer<T>>>() {
@Override
protected java.util.List<com.weibo.api.motan.rpc.Referer<T>> initialValue() {
return new ArrayList<Referer<T>>();
}
};

@Override
public Response call(Request request, LoadBalance<T> loadBalance) {

List<Referer<T>> referers = selectReferers(request, loadBalance);
if (referers.isEmpty()) {
throw new MotanServiceException(String.format("FailoverHaStrategy No referers for request:%s, loadbalance:%s", request,
loadBalance));
}
URL refUrl = referers.get(0).getUrl();
// 先使用 method 的配置
int tryCount =
refUrl.getMethodParameter(request.getMethodName(), request.getParamtersDesc(), URLParamType.retries.getName(),
URLParamType.retries.getIntValue());
// 如果有问题,则设置为不重试
if (tryCount < 0) {
tryCount = 0;
}
// 只有 failover 策略才会有重试
for (int i = 0; i <= tryCount; i++) {
Referer<T> refer = referers.get(i % referers.size());
try {
request.setRetries(i);
return refer.call(request);
} catch (RuntimeException e) {
// 对于业务异常,直接抛出
if (ExceptionUtil.isBizException(e)) {
throw e;
} else if (i >= tryCount) {
throw e;
}
LoggerUtil.warn(String.format("FailoverHaStrategy Call false for request:%s error=%s", request, e.getMessage()));
}
}

throw new MotanFrameworkException("FailoverHaStrategy.call should not come here!");
}

protected List<Referer<T>> selectReferers(Request request, LoadBalance<T> loadBalance) {
List<Referer<T>> referers = referersHolder.get();
referers.clear();
loadBalance.selectToHolder(request, referers);
return referers;
}

}

其二的高可用策略是 failover,实现相对复杂一些,容忍在重试次数内的失败调用。这也是 motan 提供的默认策略。

其他集群相关的知识点

在 Dubbo 中也有 cluster 这一分层,除了 loadbalance 和 ha 这两层之外还包含了路由(Router)用来做读写分离,应用隔离;合并结果(Merger)用来做响应结果的分组聚合。

在 SpringCloud-Netflix 中整合了 Zuul 来做服务端的负载均衡

参考资料

  1. 几种简单的负载均衡算法及其 Java 代码实现
  2. 搜索业务和技术介绍及容错机制
分享到

JAVA 拾遗 --JPA 二三事

记得前几个月,spring4all 社区刚搞过一次技术话题讨论:如何对 JPA 或者 MyBatis 进行技术选型?传送门:http://www.spring4all.com/article/391 由于平时工作接触较多的是 JPA,所以对其更熟悉一些,这一篇文章记录下个人在使用 JPA 时的一些小技巧。补充说明:JPA 是一个规范,本文所提到的 JPA,特指 spring-data-jpa。

tips:阅读本文之前,建议了解值对象和实体这两个概念的区别。

使用 @Embedded 关联一对一的值对象

现实世界有很多一对一的关联关系,如人和身份证,订单和购买者… 而在 JPA 中表达一对一的关联,通常有三种方式。下面就以订单(Order)和购买者(CustomerVo)为例来介绍这三种方式,这里 CustomerVo 的 Vo 指的是 Value Object。

字段平铺

这可能是最简单的方式了,由于一对一关联的特殊性,完全可以在 Order 类中,使用几个字段记录 CustomerVo 的属性。

1
2
3
4
5
6
7
8
public class Order {
/* 其他字段 */
...
/* Customer 相关字段 */
private int customerId;
private String customerName;
private String customerMobile;
}

实际上大多数人就是这么做的,甚至都没有意识到这三个字段其实是属于同一个实体类。这种形式优点是很明显的:简单;缺点也是很明显的,这不符合 OO 的原则,且不利于统一检索和维护 CustomerVo 信息。

使用 @OneToOne

1
2
3
4
public class Order {
@OneToOne
private CustomerVo customerVo;
}

这么做的确更“面向对象”了,但代价似乎太大了,我们需要在数据库中额外维护一张 CustomerVo 表,关联越多,代码处理起来就越麻烦,得不偿失。

使用 @Embedded

那有没有能中和上述矛盾的方案呢?引出 @Embedded 这个注解。分析下初始需求,我们发现:CustomerVo 仅仅是作为一个值对象,并不是一个实体(这里牵扯到一些领域驱动设计的知识,值对象的特点是:作为实体对象的修饰,即 CustomerVo 这个整体是 Order 实体的一个属性;不变性,CustomerVo 一旦生成后便不可被修改,除非被整体替换)

@Embedded 注解便是内嵌值对象最好的表达形式。

1
2
3
4
5
@Entity
public class Order {
@Embedded
private CustomerVo customerVo;
}
1
2
3
4
5
6
@Embeddable
public class CustomerVo {
private int customerId;
private String customerName;
private String customerMobile;
}

Order 拥有 @Entity 注解,表明其是 DDD 中的实体;而 CustomerVo 拥有 @Embeddable 注解,表明其是 DDD 中的值对象。这也是为什么我一直在表达这样一种观点:JPA 是对 DDD 很好的实践的。

关于实体类的设计技巧,在曹祖鹏老师的 github 中可以看到很成熟的方案,可能会颠覆你对实体类设计的认知:https://github.com/JoeCao/qbike/。

使用 @Convert 关联一对多的值对象

说到一对多,第一反应自然是使用 @OneToMany 注解。的确,我自己在项目中也主要使用这个注解来表达一对多的关联,但这里提供另一个思路,来关联一对多的值对象。

以商品和商品组图来举例。

使用 @OneToMany

还是先想想我们原来会怎么做,保存一个 List, 一种方式是这样

1
2
3
4
public class Goods {
// 以逗号分隔
private String pictures;
}

使用字符串存储,保存成 JSON 数组的形式,或者以逗号分隔都行。

如果图片还要保存顺序,缩略图,那就必须要得使用一对多的关联了。

1
2
3
4
5
@Entity
public class Goods {
@OneToMany
private List<GoodsPicture> goodsPictures;
}
1
2
3
4
5
6
@Entity
public class GoodsPicture {
private String path;
private Integer index;
private String thumbnail;
}

我们应当发现这样的劣势是什么,从设计的角度来看:我们并不想单独为 GoodsPicture 单独建立一张表,正如前面使用 String pictures 来表示 List 一样,这违反了数据库设计的第一范式,但这对于使用者来说非常方便, 这是关系型数据库的表达能力有限而进行的妥协 。关于这一点我曾和芋艿,曹大师都进行过讨论,并达成了一致的结论:数据库中可以保存 JSON,使用时在应用层进行转换。

使用 JSON 存储复杂对象

1
2
3
4
5
6
7
8
9
@Entity
public class Goods {
/**
* 图片 JSON
* {@link GoodsPicture}
*/
@Column(columnDefinition = "text")
private String goodsPictures;
}

使用 @Convert

上述的 String 使得在数据库层面少了一张表,使得 Goods 和 GoodsPictures 的关联更容易维护,但也有缺点:单纯的 String goodsPictures 对于使用者来说毫无含义,必须经过应用层的转换才可以使用。而 JPA 实际上也提供了自定义的转换器来帮我们自动完成这一转换工作,这便到了 @Convert 注解派上用场的时候了。

1 声明 Convert 类

1
2
3
4
5
6
@Entity
public class Goods {
@Convert(converter = PicturesWrapperConverter.class)
@Column(columnDefinition = "text")
private PicturesWrapper picturesWrapper;
}

2 设置转换类 PicturesWrapperConverter

1
2
3
4
5
6
7
8
9
10
public class PicturesWrapperConverter implements AttributeConverter<PicturesWrapper, String> {
@Override
public String convertToDatabaseColumn(PicturesWrapper picturesWrapper) {
return JSON.toJSONString(picturesWrapper);
}
@Override
public PicturesWrapper convertToEntityAttribute(String dbData) {
return JSON.parseObject(dbData, PicturesWrapper.class);
}
}

PicturesWrapperConverter 实现了 AttributeConverter<X,Y> 接口,它表明了如何将 PicturesWrapper 转换成 String 类型。这样的好处是显而易见的,对于数据库而言,它知道 String 类型如何保存;对于 Goods 的使用者而言,也只关心 PicturesWrapper 的格式,并不关心它如何持久化。

1
2
3
public class PicturesWrapper {
List<GoodsPicture> goodsPictures;
}

对于 List 的保存,我暂时只找到了这种方式,借助一个 Wrapper 对象去存储一个 List 对象。没有找到直接持久化 List 的方式,如果可以实现这样的方式,会更好一些:

1
2
3
4
5
6
@Entity
public class Goods {
@Convert(converter = SomeConverter.class)
@Column(columnDefinition = "text")
List<GoodsPicture> goodsPictures;
}

但 converter 无法获取到 List 的泛型参数 GoodsPicture,在实践中没找到方案来解决这一问题,只能退而求其次,使用一个 Wrapper 对象。

与 OneToMany 对比,这样虽然使得维护变得灵活,但也丧失了查找的功能,我们将之保存成了 JSON 的形式,导致其不能作为查询条件被检索。

使用 orphanRemoval 来删除值对象

你可能有两个疑问:1 在实际项目中,不是不允许对数据进行物理删除吗? 2 删除对象还不简单,JPA 自己不是有 delete 方法吗?

关于第一点,需要区分场景,一般实体不允许做物理删除,而是用标记位做逻辑删除,也有部分不需要追溯历史的实体可以做物理删除,而值对象一般而言是可以做物理删除的,因为它只是属性而已。

第二点就有意思了,delete 不就可以直接删除对象吗,为什么需要介绍 orphanRemoval 呢?

以活动和礼包这个一对多的关系来举例。

1
2
3
4
5
6
7
8
@Entity
public class Activity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "activity")
private List<GiftPackVo> giftPackVos;
}
1
2
3
4
5
6
7
8
9
10
@Entity
public class GiftPackVo {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String name;
@ManyToOne
@JoinColumn(name = "activity_id")
private Activity activity;
}

这是一个再简单不过的一对多关系了,唯一可能觉得陌生的便是这个属性了 orphanRemoval = true 。

如果想要删除某个活动下的某个礼包,在没有 orphanRemoval 之前,你只能这么做:

GiftPackVoRepository.delete(GiftPackVo);

但其实这违反了 DDD 中的聚合根模式,GiftPackVo 只是一个值对象,其不具备实体的生命周期,删除一个礼包其实是一个不准确的做法,应当是删除某一个活动下的某一个礼包,对礼包的维护,应当由活动来负责。也就是说:应该借由 Activity 删除 GiftPackVo。使用 orphanRemoval 便可以完成这一操作,它表达这样的含义:内存中的某个 Activity 对象属于持久化态,对 List 的移除操作,将被直接认为是删除操作。

于是删除某个“name = 狗年新春大礼包”的礼包便可以这样完成:

1
2
3
Activity activity = activityRepository.findOne(1);
activity.getGiftPackVos().removeIf(giftPackVo -> "狗年新春大礼包".equals(giftPackVo.getName()));
activityRepository.save(activity);

整个代码中只出现了 activityRepository 这一个仓储接口。

使用 @Version 来实现乐观锁

乐观锁一直是保证并发问题的一个有效途径,spring data jpa 对 @Version 进行了实现,我们给需要做乐观锁控制的对象加上一个 @Version 注解即可。

1
2
3
4
5
@Entity
public class Activity {
@Version
private Integer version;
}

我们在日常操作 Activity 对象时完全不需要理会 version 这个字段,当做它不存在即可,spring 借助这个字段来做乐观锁控制。每次创建对象时,version 默认值为 0,每次修改时,会检查对象获取时和保存时的 version 是否相差 1,转化为 sql 便是这样的语句:update activity set xx = xx,yy = yy,version= 10 where id = 1 and version = 9; 然后通过返回影响行数来判断是否更新成功。

测试乐观锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Service
public class ActivityService {
@Autowired
ActivityRepos activityRepos;

public void test(){
Activity one = activityRepos.findOne(1);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
one.setName("xx"+ new Random().nextInt());
activityRepos.save(one);
}
}

当 test 方法被并发调用时,可能会存在并发问题。控制台打印出了更新信息

1
2
3
4
5
2018-02-14 23:44:25.373  INFO 16256 --- [nio-8080-exec-2] jdbc.sqltiming                           : update activity set name='xx-1863402614', version=1 
where id=1 and version=0
2018-02-14 23:44:25.672 INFO 16256 --- [nio-8080-exec-4] jdbc.sqltiming : update activity set name='xx-1095770865', version=1
where id=1 and version=0
org.hibernate.StaleStateException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1

表面上看出现的是 StaleStateException,但实际捕获时,如果你想 catch 该异常,根本没有效果,通过 debug 信息,可以发现,真正的异常其实是 ObjectOptimisticLockingFailureException(以 Mysql 为例,实际可能和数据库方言有关,其他数据库未测试)。

1
2
3
4
5
6
7
8
9
@RequestMapping("/test")
public void test(){
try{
activityService.test();
}catch (ObjectOptimisticLockingFailureException oolfe){
System.out.println("捕获到乐观锁并发异常");
oolfe.printStackTrace();
}
}

在 Controller 层尝试捕获该异常,控制输出如下:

1
2
捕获到乐观锁并发异常
org.springframework.orm.ObjectOptimisticLockingFailureException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1; nested exception is org.hibernate.StaleStateException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1

成功捕获到了并发冲突,这一切都是 @Version 帮我们完成的,非常方便,不需要我们通过编码去实现乐观锁。

总结

本文简单聊了几个个人感触比较深的 JPA 小技巧,JPA 真的很强大,也很复杂,可能还有不少“隐藏”的特性等待我们挖掘。它不仅仅是一个技术框架,本文的所有内容即使不被使用,也无伤大雅,但在领域驱动设计等软件设计思想的指导下,它完全可以实践的更好。

分享到