Fork me on GitHub

类加载过程

原文来自《码出高效: Java 开发手册》

代码地址: easy-coding

Java 的类加载器是一个运行时核心基础设施模块,如图 4-4 所示,主要是在启动之初进行类的 Load、Link 和 Init,即加载、链接、初始化。

第一步,Load 阶段读取类文件产生二进制流,并转换为特定的数据结构,初步校验 cafe babe 魔法数、常量池、文件长度、是否有父类等,然后创建对应类的 java.lang.Class 实例。

第二步,Link 阶段包括验证、准备、解析三个步骤。验证是更详细的校验,比如 final 是否合规、类型是否正确、静态变量是否合理等;准备阶段是为静态变量分配内存,并设定默认值;解析阶段是解析类和方法,确保类与类之间的相互引用正确性,完成内存结构布局。

第三步,Init 阶段执行类构造器 <clinit> 方法,如果赋值运算是通过其他类的静态方法来完成的,那么会马上解析另一个类,在虚拟机栈中执行完毕后通过返回值进行赋值。

代码

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
public class ClassTest {
// 数组类型有一个魔法属性: length 来获取数组长度
private static int[] array = new int[3];
private static int length = array.length;

// 任何小写 class 定义的类,也有一个魔法属性: class,来获取此类的大写 Class 类对象
private static Class<One> one = One.class;
private static Class<Another> another = Another.class;

public static void main(String[] args) throws Exception {
// 通过newInstance 方法创建 One 和 Another 的类对象( 第 1 处 )
One oneObject = one.newInstance();
oneObject.call();

Another anotherObject = another.newInstance();
anotherObject.speak();

// 通过 one 这个大写的 Class 对象,获取私有成员属性对象 Field( 第 2 处 )
Field privateFieldInOne = one.getDeclaredField("inner");

// 设置私有对象可以访问和修改( 第 3 处 )
privateFieldInOne.setAccessible(true);

privateFieldInOne.set(oneObject, "world changed.");
// 成功修改类的私有属性 inner 变量值为 world changed.
System.out.println(oneObject.getInner());
}


}

class One {
private String inner = "time files.";

public void call() {
System.out.println("hello world.");
}

public String getInner() {
return inner;
}
}

class Another {
public void speak() {
System.out.println("easy coding.");
}
}
阅读更多...

动画推荐《我的三体》

《我的三体》是一部粉丝自制的动画番剧,改编自刘慈欣的科幻小说《三体》。旨在以方的风格把《三体》的故事改编成动画。由神游八方始更于2014年,截至2015年10月6日,共拍摄完成并发布了11集 ,第一季完结。

豆瓣地址: https://movie.douban.com/subject/27624727/

视频地址: https://space.bilibili.com/614801/video

逛b站的时候发现了豆瓣2015评分最高的十部短片合集,也非常推荐观看

【2015】豆瓣电影年度榜单 评分最高的十部短片

我是在一个周末花了一天时间一口气看完的,画质完美的诠释了什么叫技术爆炸,画质从第一季第一集的粗糙逐步提升到第二季最后一集的惊艳。

《我的三体》第一季和第二季在豆瓣这种苛刻的地方居然都能拿到9分以上的高分,说明这部动画真的非常棒👍,非常推荐观看。

第一季开始是这样的

第二季是这样的

宇宙社会学:人类当前的科技水平和社会状况下,从两条不证自明的基本公理出发(一、生存是文明的第一需要。二、文明不断增长和扩张,但宇宙中的物质总量保持不变),借由引入两个重要概念——猜疑链技术爆炸,从理论上建立起的一套关于描述当前宇宙社会大图景的大体系的一门学科。

当你显露出本领的时候,别人把你当成救世主,当你告诉别人你做不到的时候,他们恨不得把你踩进尘埃里。人就是这样,这样的不讲理。罗辑是善良的,他愿意为了人类尽自己最大的努力去改变,即使别人对他指指点点,仍旧心存善意,这样的人可敬。

当你显露出本领的时候,别人把你当成救世主,当你告诉别人你做不到的时候,他们恨不得把你踩进尘埃里。人就是这样,这样的不讲理。罗辑是善良的,他愿意为了人类尽自己最大的努力去改变,即使别人对他指指点点,仍旧心存善意,这样的人可敬。— 豆瓣 小萝卜

解决Hash冲突的几种方法

原文地址: https://blog.csdn.net/u012104435/article/details/47951357

开放地址法:

1.线性探测法:ThreadLocalMap

线性再散列法是形式最简单的处理冲突的方法。插入元素时,如果发生冲突,算法会简单的从该槽位置向后循环遍历hash表,直到找到表中的下一个空槽,并将该元素放入该槽中(会导致相同hash值的元素挨在一起和其他hash值对应的槽被占用)。查找元素时,首先散列值所指向的槽,如果没有找到匹配,则继续从该槽遍历hash表,直到:(1)找到相应的元素;(2)找到一个空槽,指示查找的元素不存在,(所以不能随便删除元素);(3)整个hash表遍历完毕(指示该元素不存在并且hash表是满的)

用线性探测法处理冲突,思路清晰,算法简单,但存在下列缺点:
① 处理溢出需另编程序。一般可另外设立一个溢出表,专门用来存放上述哈希表中放不下的记录。此溢出表最简单的结构是顺序表,查找方法可用顺序查找。
② 按上述算法建立起来的哈希表,删除工作非常困难。如果将此元素删除,查找的时会发现空槽,则会认为要找的元素不存在。只能标上已被删除的标记,否则,将会影响以后的查找。
③ 线性探测法很容易产生堆聚现象。所谓堆聚现象,就是存入哈希表的记录在表中连成一片。按照线性探测法处理冲突,如果生成哈希地址的连续序列愈长 ( 即不同关键字值的哈希地址相邻在一起愈长 ) ,则当新的记录加入该表时,与这个序列发生冲突的可能性愈大。因此,哈希地址的较长连续序列比较短连续序列生长得快,这就意味着,一旦出现堆聚 ( 伴随着冲突 ) ,就将引起进一步的堆聚。

2.线性补偿探测法

线性补偿探测法的基本思想是:将线性探测的步长从 1 改为 Q ,即将上述算法中的
hash = (hash + 1) % m 改为:hash = (hash + Q) % m = hash % m + Q % m,而且要求 Q 与 m 是互质的,以便能探测到哈希表中的所有单元。
【例】 PDP-11 小型计算机中的汇编程序所用的符合表,就采用此方法来解决冲突,所用表长 m = 1321 ,选用 Q = 25 。

3.伪随机探测

随机探测的基本思想是:将线性探测的步长从常数改为随机数,即令: hash = (hash + RN) % m ,其中 RN 是一个随机数。在实际程序中应预先用随机数发生器产生一个随机序列,将此序列作为依次探测的步长。这样就能使不同的关键字具有不同的探测次序,从而可以避 免或减少堆聚。基于与线性探测法相同的理由,在线性补偿探测法和随机探测法中,删除一个记录后也要打上删除标记。

拉链法

拉链法 : hashmap

拉链法的优点
与开放定址法相比,拉链法有如下几个优点:
①拉链法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短;
②由于拉链法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况;
③开放定址法为减少冲突,要求装填因子α较小,故当结点规模较大时会浪费很多空间。而拉链法中可取α≥1,且结点较大时,拉链法中增加的指针域可忽略不计,因此节省空间;
④在用拉链法构造的散列表中,删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。

拉链法的缺点
 拉链法的缺点是:指针需要额外的空间,故当结点规模较小时,开放定址法较为节省空间,而若将节省的指针空间用来扩大散列表的规模,可使装填因子变小,这又减少了开放定址法中的冲突,从而提高平均查找速度。

再散列(双重散列,多重散列)

当发生冲突时,使用第二个、第三个、哈希函数计算地址,直到无冲突时。缺点:计算时间增加。

建立一个公共溢出区

假设哈希函数的值域为[0,m-1],则设向量HashTable[0..m-1]为基本表,另外设立存储空间向量OverTable[0..v]用以存储发生冲突的记录。

Java 8中Stream API的这些奇技淫巧!你都Get到了吗?

原文地址: java8-Stream集合操作快速上手

Stream简介

1、Java 8引入了全新的Stream API。这里的Stream和I/O流不同,它更像具有Iterable的集合类,但行为和集合类又有所不同。

2、stream是对集合对象功能的增强,它专注于对集合对象进行各种非常便利、高效的聚合操作,或者大批量数据操作。

3、只要给出需要对其包含的元素执行什么操作,比如 “过滤掉长度大于 10 的字符串”、“获取每个字符串的首字母”等,Stream 会隐式地在内部进行遍历,做出相应的数据转换。

为什么要使用Stream

1、函数式编程带来的好处尤为明显。这种代码更多地表达了业务逻辑的意图,而不是它的实现机制。易读的代码也易于维护、更可靠、更不容易出错。

2、高端

实例数据源

Filter

1、遍历数据并检查其中的元素时使用。

2、filter接受一个函数作为参数,该函数用Lambda表达式表示。

Map

1、map生成的是个一对一映射,for的作用

2、比较常用

3、而且很简单

阅读更多...

B站UP主推荐-阿幕降临(原创)

今天逛b站发现一个动画up主的视频很有意思,通过沙雕熊猫人动画非常幽默诙谐地讽刺了当今社会一些热点问题

被大家称为b站鲁迅,看了之后特意推荐下

视频地址: https://space.bilibili.com/38351330

当代我国年轻人一天(空闲时间)分配

讽刺了年轻人天天治疗拖延就是不去学习,一直等到最后期限才学一下

住在底层的小红上当了

讽刺了层层剥削的现象

中国多数穷二代青年的现实困境

穷人的无奈

最受长辈欢迎的7号小朋友后来果然混的风生水起

不负责任的老师将学生教育成一个样子,没有个性

鬼才销售 卖火柴的小女孩

讽刺无脑跟风

小红是一个泪点低的人…

讽刺玻璃心,圣母心

如何利用费曼学习法提高效率?(T君)

视频地址: https://www.bilibili.com/video/av64850144

总结

费曼技术

①向不熟悉某议题的人解释该议题,使用他们能理解的方式及最简单的语言向他们解释。
②发现自己不能理解的地方或不能解释某议题的地方。
③回头查看资讯来源并研读自己弱点的地方,直到能用简单的语言来解释它。
④重复前面三项步骤,直到能够专精这个议题。

使用费曼技术由两个主要需达成的目标:简单并简洁。

“聪明的傻瓜能把事情搞得更大更复杂更暴力但往反方向发展则需要一点天分以及巨大的勇气。”

— E.F.舒马赫

举例子是一个能够增进专精程度并学习同理的好方法。

使用费曼技术最大的好处是:它能使你真正地了解任何你学习的事物、使你做出深思熟虑并有智慧的决定、更为简单地将知识应用到实际问题时教导能力的进步、以及对于议题进行批判思考的能力提升,唯一被取舍的是时间和精力。即使如此,之后投资的时间会比一开始少很多。

什么时候需要使用费曼技术以进行深度学习?
当我们面临改变人生的抉择时,以及我们在努力工作时。

什么是整洁的代码?什么是肮脏的代码?

原文地址: https://mp.weixin.qq.com/s/PDOZR8YirIo_cis-8S0wgQ

  • 命名的艺术
  • 注释
  • 函数
  • 测试

写出整洁的代码,是每个程序员的追求。《clean code》指出,要想写出好的代码,首先得知道什么是肮脏代码、什么是整洁代码;然后通过大量的刻意练习,才能真正写出整洁的代码。

WTF/min是衡量代码质量的唯一标准,Uncle Bob在书中称糟糕的代码为沼泽(wading),这只突出了我们是糟糕代码的受害者。国内有一个更适合的词汇:屎山,虽然不是很文雅但是更加客观,程序员既是受害者也是加害者。

对于什么是整洁的代码,书中给出了大师们的总结:

  • Bjarne Stroustrup:优雅且高效;直截了当;减少依赖;只做好一件事
  • Grady booch:简单直接
  • Dave thomas:可读,可维护,单元测试
  • Ron Jeffries:不要重复、单一职责,表达力(Expressiveness)

其中,我最喜欢的是表达力(Expressiveness)这个描述,这个词似乎道出了好代码的真谛:用简单直接的方式描绘出代码的功能,不多也不少。

本文记录阅读《clean code》之后个人“深有同感”或者“醍醐灌顶”的一些观点。

命名的艺术

坦白的说,命名是一件困难的事情,要想出一个恰到好处的命名需要一番功夫,尤其我们的母语还不是编程语言所通用的英语。不过这一切都是值得了,好的命名让你的代码更直观,更有表达力。

好的命名应该有下面的特征:

名副其实

好的变量名告诉你:是什么东西,为什么存在,该怎么使用

如果需要通过注释来解释变量,那么就先得不那么名副其实了。

下面是书中的一个示例代码,展示了命名对代码质量的提升

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# bad code
def getItem(theList):
ret = []
for x in theList:
if x[0] == 4:
ret.append(x)
return ret

# good code
def getFlaggedCell(gameBoard):
'''扫雷游戏,flagged: 翻转'''
flaggedCells = []
for cell in gameBoard:
if cell.IsFlagged():
flaggedCells.append(cell)
return flaggedCells

避免误导

  • 不要挂羊头卖狗肉
  • 不要覆盖惯用缩略语

这里不得不吐槽前两天才看到的一份代码,居然使用了 l 作为变量名;而且,user居然是一个list(单复数都没学好!!)

有意义的区分

代码是写给机器执行,也是给人阅读的,所以概念一定要有区分度。

1
2
3
4
5
6
7
# bad
def copy(a_list, b_list):
pass

# good
def copy(source, destination):
pass

使用读的出来的单词

如果名称读不出来,那么讨论的时候就会像个傻鸟

使用方便搜索的命名

名字长短应与其作用域大小相对应

避免思维映射

比如在代码中写一个temp,那么读者就得每次看到这个单词的时候翻译成其真正的意义

注释

表达力的代码是无需注释的。

The proper use of comments is to compensate for our failure to express ourself in code.

注释的适当作用在于弥补我们用代码表达意图时遇到的失败,这听起来让人沮丧,但事实确实如此。The truth is in the code, 注释只是二手信息,二者的不同步或者不等价是注释的最大问题。

书中给出了一个非常形象的例子来展示:用代码来阐述,而非注释

1
2
3
4
5
6
bad
// check to see if the employee is eligible for full benefit
if ((employee.flags & HOURLY_FLAG) && (employee.age > 65))

good
if (employee.isEligibleForFullBenefits())

因此,当想要添加注释的时候,可以想想是否可以通过修改命名,或者修改函数(代码)的抽象层级来展示代码的意图。

当然,也不能因噎废食,书中指出了以下一些情况属于好的注释

  1. 法务信息
  2. 对意图的注释,为什么要这么做
  3. 警示
  4. TODO注释
  5. 放大看似不合理之物的重要性

其中个人最赞同的是第2点和第5点,做什么很容易通过命名表达,但为什么要这么做则并不直观,特别涉及到专业知识、算法的时候。另外,有些第一感觉“不那么优雅”的代码,也许有其特殊愿意,那么这样的代码就应该加上注释,说明为什么要这样,比如为了提升关键路径的性能,可能会牺牲部分代码的可读性。

最坏的注释就是过时或者错误的注释,这对于代码的维护者(也许就是几个月后的自己)是巨大的伤害,可惜除了code review,并没有简单易行的方法来保证代码与注释的同步。

函数

函数的单一职责

一个函数应该只做一件事,这件事应该能通过函数名就能清晰的展示。判断方法很简单:看看函数是否还能再拆出一个函数。

函数要么做什么do_sth, 要么查询什么query_sth。最恶心的就是函数名表示只会query_sth, 但事实上却会do_sth, 这使得函数产生了副作用。比如书中的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class UserValidator {
private Cryptographer cryptographer;
public boolean checkPassword(String userName, String password) {
User user = UserGateway.findByName(userName);
if (user != User.NULL) {
String codedPhrase = user.getPhraseEncodedByPassword();
String phrase = cryptographer.decrypt(codedPhrase, password);
if ("Valid Password".equals(phrase)) {
Session.initialize();
return true;
}
}
return false;
}
}

函数的抽象层级

每个函数一个抽象层次,函数中的语句都要在同一个抽象层级,不同的抽象层级不能放在一起。比如我们想把大象放进冰箱,应该是这个样子的:

1
2
3
4
def pushElephantIntoRefrige():
openRefrige()
pushElephant()
closeRefrige()

函数里面的三句代码在同一个层级(高度)描述了要完成把大象放进冰箱这件事顺序相关的三个步骤。显然,pushElephant这个步骤又可能包含很多子步骤,但是在pushElephantIntoRefrige这个层级,是无需知道太多细节的。

当我们想通过阅读代码的方式来了解一个新的项目时,一般都是采取广度优先的策略,自上而下的阅读代码,先了解整体结构,然后再深入感兴趣的细节。如果没有对实现细节进行良好的抽象(并凝练出一个名副其实的函数),那么阅读者就容易迷失在细节的汪洋里。

某种程度看来,这个跟金字塔原理也很像

每一个层级都是为了论证其上一层级的观点,同时也需要下一层级的支持;同一层级之间的多个论点又需要以某种逻辑关系排序。pushElephantIntoRefrige就是中心论点,需要多个子步骤的支持,同时这些子步骤之间也有逻辑先后顺序。

函数参数

函数的参数越多,组合出的输入情况就愈多,需要的测试用例也就越多,也就越容易出问题。

输出参数相比返回值难以理解,这点深有同感,输出参数实在是很不直观。从函数调用者的角度,一眼就能看出返回值,而很难识别输出参数。输出参数通常逼迫调用者去检查函数签名,这个实在不友好。

向函数传入Boolean(书中称之为 Flag Argument)通常不是好主意。尤其是传入True or False后的行为并不是一件事情的两面,而是两件不同的事情时。这很明显违背了函数的单一职责约束,解决办法很简单,那就是用两个函数。

Dont repear yourself

在函数这个层级,是最容易、最直观实现复用的,很多IDE也难帮助我们讲一段代码重构出一个函数。

不过在实践中,也会出现这样一种情况:一段代码在多个方法中都有使用,但是又不完全一样,如果抽象成一个通用函数,那么就需要加参数、加if else区别。这样就有点尴尬,貌似可以重构,但又不是很完美。

造成上述问题的某种情况是因为,这段代码也违背了单一职责原则,做了不只一件事情,这才导致不好复用,解决办法是进行方法的细分,才能更好复用。也可以考虑template method来处理差异的部分。

测试

非常惭愧的是,在我经历的项目中,测试(尤其是单元测试)一直都没有得到足够的重视,也没有试行过TDD。正因为缺失,才更感良好测试的珍贵。

我们常说,好的代码需要有可读性、可维护性、可扩展性,好的代码、架构需要不停的重构、迭代,但自动化测试是保证这一切的基础,没有高覆盖率的、自动化的单元测试、回归测试,谁都不敢去修改代码,只能任其腐烂。

即使针对核心模块写了单元测试,一般也很随意,认为这只是测试代码,配不上生产代码的地位,以为只要能跑通就行了。这就导致测试代码的可读性、可维护性非常差,然后导致测试代码很难跟随生产代码一起更新、演化,最后导致测试代码失效。所以说,脏测试 - 等同于 - 没测试。

因此,测试代码的三要素:可读性,可读性,可读性。

对于测试的原则、准则如下:

  • You are not allowed to write any production code unless it is to make a failing unit test pass. 没有测试之前不要写任何功能代码
  • You are not allowed to write any more of a unit test than is sufficient to fail; and compilation failures are failures. 只编写恰好能够体现一个失败情况的测试代码
  • You are not allowed to write any more production code than is sufficient to pass the one failing unit test. 只编写恰好能通过测试的功能代码

测试的FIRST准则:

  1. 快速(Fast)测试应该够快,尽量自动化。
  2. 独立(Independent) 测试应该应该独立。不要相互依赖
  3. 可重复(Repeatable) 测试应该在任何环境上都能重复通过。
  4. 自我验证(Self-Validating) 测试应该有bool输出。不要通过查看日志这种低效率方式来判断测试是否通过
  5. 及时(Timely) 测试应该及时编写,在其对应的生产代码之前编写

这样设计 Java 异常更优雅,赶紧学!

原文地址: 这样设计 Java 异常更优雅,赶紧学!

导语

异常处理是程序开发中必不可少操作之一,但如何正确优雅的对异常进行处理确是一门学问,笔者根据自己的开发经验来谈一谈我是如何对异常进行处理的。

由于本文只作一些经验之谈,不涉及到基础知识部分,如果读者对异常的概念还很模糊,请先查看基础知识。

如何选择异常类型

异常的类别

正如我们所知道的,java中的异常的超类是java.lang.Throwable(后文省略为Throwable),它有两个比较重要的子类,java.lang.Exception(后文省略为Exception)和java.lang.Error(后文省略为Error),其中Error由JVM虚拟机进行管理,如我们所熟知的OutOfMemoryError异常等,所以我们本文不关注Error异常,那么我们细说一下Exception异常。

Exception异常有个比较重要的子类,叫做RuntimeException。我们将RuntimeException或其他继承自RuntimeException的子类称为非受检异常(unchecked Exception),其他继承自Exception异常的子类称为受检异常(checked Exception)。本文重点来关注一下受检异常和非受检异常这两种异常。

如何选择异常

从笔者的开发经验来看,如果在一个应用中,需要开发一个方法(如某个功能的service方法),这个方法如果中间可能出现异常,那么你需要考虑这个异常出现之后是否调用者可以处理,并且你是否希望调用者进行处理,如果调用者可以处理,并且你也希望调用者进行处理,那么就要抛出受检异常,提醒调用者在使用你的方法时,考虑到如果抛出异常时如果进行处理,相似的,如果在写某个方法时,你认为这是个偶然异常,理论上说,你觉得运行时可能会碰到什么问题,而这些问题也许不是必然发生的,也不需要调用者显示的通过异常来判断业务流程操作的,那么这时就可以使用一个RuntimeException这样的非受检异常.

好了,估计我上边说的这段话,你读了很多遍也依然觉得晦涩了。

那么,请跟着我的思路,在慢慢领会一下。

什么时候才需要抛异常

首先我们需要了解一个问题,什么时候才需要抛异常?异常的设计是方便给开发者使用的,但不是乱用的,笔者对于什么时候抛异常这个问题也问了很多朋友,能给出准确答案的确实不多。其实这个问题很简单,如果你觉得某些”问题”解决不了了,那么你就可以抛出异常了。比如,你在写一个service,其中在写到某段代码处,你发现可能会产生问题,那么就请抛出异常吧,相信我,你此时抛出异常将是一个最佳时机。

应该抛出怎样的异常

了解完了什么时候才需要抛出异常后,我们再思考一个问题,真的当我们抛出异常时,我们应该选用怎样的异常呢?究竟是受检异常还是非受检异常呢(RuntimeException)呢?我来举例说明一下这个问题,先从受检异常说起,比如说有这样一个业务逻辑,需要从某文件中读取某个数据,这个读取操作可能是由于文件被删除等其他问题导致无法获取从而出现读取错误,那么就要从redis或mysql数据库中再去获取此数据,参考如下代码,getKey(Integer)为入口程序.

public String getKey(Integer key){

​ String value;

​ try {

​ InputStream inputStream = getFiles(“/file/nofile”);

​ //接下来从流中读取key的value指

​ value = …;

​ } catch (Exception e) {

​ //如果抛出异常将从mysql或者redis进行取之

​ value = …;

​ }

}

public InputStream getFiles(String path) throws Exception {

​ File file = new File(path);

​ InputStream inputStream = null;

​ try {

​ inputStream = new BufferedInputStream(new FileInputStream(file));

​ } catch (FileNotFoundException e) {

​ throw new Exception(“I/O读取错误”,e.getCause());

​ }

​ return inputStream;

}

ok,看了以上代码以后,你也许心中有一些想法,原来受检异常可以控制义务逻辑,对,没错,通过受检异常真的可以控制业务逻辑,但是切记不要这样使用,我们应该合理的抛出异常,因为程序本身才是流程,异常的作用仅仅是当你进行不下去的时候找到的一个借口而已,它并不能当成控制程序流程的入口或出口,如果这样使用的话,是在将异常的作用扩大化,这样将会导致代码复杂程度的增加,耦合性会提高,代码可读性降低等问题。那么就一定不要使用这样的异常吗?其实也不是,在真的有这样的需求的时候,我们可以这样使用,只是切记,不要把它真的当成控制流程的工具或手段。那么究竟什么时候才要抛出这样的异常呢?要考虑,如果调用者调用出错后,一定要让调用者对此错误进行处理才可以,满足这样的要求时,我们才会考虑使用受检异常。

接下来,我们来看一下非受检异常呢(RuntimeException),对于RuntimeException这种异常,我们其实很多见,比如java.lang.NullPointerException/java.lang.IllegalArgumentException等,那么这种异常我们时候抛出呢?当我们在写某个方法的时候,可能会偶然遇到某个错误,我们认为这个问题时运行时可能为发生的,并且理论上讲,没有这个问题的话,程序将会正常执行的时候,它不强制要求调用者一定要捕获这个异常,此时抛出RuntimeException异常,举个例子,当传来一个路径的时候,需要返回一个路径对应的File对象:

public void test() {

​ myTest.getFiles(“”);

}

public File getFiles(String path) {

​ if(null == path || “”.equals(path)){

​ throw new NullPointerException(“路径不能为空!”);

​ }

​ File file = new File(path);

​ return file;

}

上述例子表明,如果调用者调用getFiles(String)的时候如果path是空,那么就抛出空指针异常(它是RuntimeException的子类),调用者不用显示的进行try…catch…操作进行强制处理.这就要求调用者在调用这样的方法时先进行验证,避免发生RuntimeException.如下:

应该选用哪种异常

通过以上的描述和举例,可以总结出一个结论,RuntimeException异常和受检异常之间的区别就是:是否强制要求调用者必须处理此异常,如果强制要求调用者必须进行处理,那么就使用受检异常,否则就选择非受检异常(RuntimeException)。一般来讲,如果没有特殊的要求,我们建议使用RuntimeException异常。

场景介绍和技术选型

架构描述

正如我们所知,传统的项目都是以MVC框架为基础进行开发的,本文主要从使用restful风格接口的设计来体验一下异常处理的优雅。

我们把关注点放在restful的api层(和web中的controller层类似)和service层,研究一下在service中如何抛出异常,然后api层如何进行捕获并且转化异常。

使用的技术是:spring-boot,jpa(hibernate),mysql,如果对这些技术不是太熟悉,读者需要自行阅读相关材料。

阅读更多...

盘口数据频繁变化,100W用户如何实时通知?

原文地址: https://mp.weixin.qq.com/s/QcPicGRkaFENP6IzMRZMaQ

继续答星球水友提问:盘口数据频繁变化,如何做缓存与推送,如何降低数据库压力?

并没有做过相关的业务,结合自己的架构经验,说说自己的思路和想法,希望对大家有启示。

一、业务抽象

(1)有很多客户端关注盘口,假设百万级别;

(2)数据量不一定很大,上市交易的股票个数,假设万级别;

(3)写的量比较大,每秒钟有很多交易发生,假设每秒百级别;

(4)计算比较复杂,有求和/分组/排序等操作;

二、潜在技术折衷

客户端与服务端连接如何选型?

首先,盘口客户端与服务器建立TCP长连接,而不是每次请求都建立与销毁短连接,能极大提升性能,降低服务器压力。

业务的实时性如何满足?

盘口业务,对数据实时性的要求较高,服务端可以通过TCP长连接推送,保证消息的实时性。

由于推送量级巨大,可以独立推送集群,专门实施推送。推送集群独立化之后,增加推送服务器数量,就可以线性提升推送能力。

如上图所示,假设有100W用户接收实时推送:

  • 搭建专门的推送集群,维护与客户端的tcp长连接,实时推送
  • 每台推送服务维护10W长连接,10台推送服务即可服务100W用户
  • 推送集群业务集群之间,通过MQ解耦,推送集群只单纯的推送消息,无任何业务逻辑计算,推送消息的内容,都是业务集群计算好的

推送服务最大的瓶颈是,如何将一条消息,最快的推送给与之连接的10W个客户端?

  • 如果消息量不大,例如几秒钟一个消息,可以开多线程,例如100个线程,并发推送

画外音:**对应水友提到的,如果量不大,可以成交一笔推送一笔。

  • 如果消息量过大,例如一秒钟几百个消息,可以将消息暂存一秒,批量推送

画外音:对应水友提到的,如果消息量巨大,批量推送是很好的方法。

数据量,写入量,扩展性如何满足?

股票个数较少,数据量不是瓶颈。

流水数据写入量,每秒百级别,甚至千级别,数据库写性能也不是瓶颈,理论上一个库可以抗住。

假如每秒写入量达到万级别,可以在数据库层面实施水平切分,将不同股票的流水拆到不同水平切分的库里去,就能线性增加数据库的写入量。

画外音:**水平拆分后,同一个股票,数据在同一个库里,不同股票,可能在不同的库里,理论上不会有跨库查询的需求。

如果每秒写入量达到十万,百万级别,还可以加入MQ缓冲请求,削峰填谷,保护数据库。

如论如何,根据本业务的数据量与写入量,单库应该是没有问题的。

复杂的业务逻辑操作,如何满足?

本业务的写入量不大,但读取量很大,肯定不能每个读取请求都sum/group by/order by,这样数据库肯定扛不住。

水友已经想到了,可以用缓存来降低数据库的压力,但担心“随着时间的推移,这个偏差势必会慢慢放大”。

关于缓存的一致性的放大,可以这么搞:

  • 做一个异步的线程,每秒钟访问一次数据库,将复杂的业务逻辑计算出来,放入高可用缓存
  • 所有的读请求不再耦合业务逻辑计算,都直接从高可用缓存读结果

如此一来,复杂业务逻辑的计算,每秒钟只会有一次

带来的问题是,一秒内可能有很多流水写入数据库,但不会实时的反应到缓存里,用户最差情况下,会读到一秒前的盘口数据

无论如何,这是一个性能与一致性的设计折衷。

上面的所有方案,都是基于在线客户量级巨大,推送消息巨大的前提下,采用推送方案。很多时候,工程师都会妄加猜测,把问题想得很复杂,把方案搞得很复杂。

如果在线用户量很小,用户能够接受的盘口时延较长(例如5s),完全可以采用轮询拉取方案:

(1)取消整个推送集群与MQ集群;

(2)盘口数据,异步线程每1s写入高可用缓存一次;

(3)客户端每5s轮询拉取最新的盘口数据,都只从缓存中拉取;

搞定!

反正,肯定不能每个读请求都sum/group by/order by扫库计算,这个是最需要优化的。

三、总结

  • 长连接比短连接性能好很多倍
  • 推送量巨大时,推送集群需要与业务集群解耦
  • 推送量巨大时,并发推送与批量推送是一个常见的优化手段
  • 写入量巨大时,水平切分能够扩容,MQ缓冲可以保护数据库
  • 业务复杂,读取量巨大时,加入缓存,定时计算,能够极大降低数据库压力

留言

本人干过亿级用户的交易所系统,全部干在内存里做,udp组播消息总线,无压力。

本人在排名全球前十的数字币交易所,当前架构,核心业务数据几乎已经放弃DB数据查询。此前用SQLServer的BCP持久化速度也跟不上。
沈总的架构方向完全OK,但是,币圈交易所,在支持程序高频交易的情况下,撮合,推送都不是问题,完成实时清算是最大的难题。

以前的行情软件看到的盘口数据,一级行情是6秒一次推送,二级是3秒一次推送。行情原始数据都在内存,然后每有新的行情快照,根据不同的业务,生成加工后的数据。然后前端会有一定逻辑,会把需要的数据,发送订阅请求。服务器端针对前端的请求,会有各种行情计算业务的服务器处理不同类型的请求。这种业务最大的特点就是用户间不用交互,全是服务器推送数据,只是要的数据种类不一样。这样我们就可以提前根据业务类型加工数据,然后用户需要时,就没有什么计算了。多级缓存,数据还一致。用户数,靠堆服务器就好。数据量并没有想象的那么多,你能看到的,就是服务器给你的全部

  • Copyrights © 2015-2023 高行行
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信