常见的设计思路

设计并无定规,但是成功的设计总有值得借鉴的地方。
计算机科学博大精深,其中包含了大量优秀的设计理念,近期学习偶有所获,在此记下。

缓存

缓存在IT技术中非常常见,它的定义是什么?
我认为缓存技术的初衷是解决了两个问题:

  1. 减小计算机CPU运算速度与存储介质IO速度的巨大差异
  2. 存放中间运算结果
    除了最初的cpu与内存间的缓存,陆续又涌现出内存与硬盘间、客户端与浏览器端的缓存。
    但是作用无外乎这两种:
  3. 减小不同部件之间的处理速度差异
  4. 暂时存放结果
    而目的只有一个:加快处理速度,减少响应时间。
    原理则是空间换时间,在IO速度快的存储介质上保存冗余的数据备份,就可以避免读低速的存储介质或者重复复杂的运算。
    缓存有2个基本问题需要解决:
  5. 缓存容量有限
    缓存一般采用IO快的存储介质,容量相对较小,一般存不了所有数据,缓存满了怎么办?很简单,清除掉部分数据就可以了,但是清除哪些比较合适呢?
    常见的策略有:
    • FIFO:先进先出,淘汰年龄最大的数据 (队列)
    • LRU:Least Recently Used,淘汰最近没用过的 (链表)
    • LFU:Least Frequently Used,淘汰使用频率最低的 (SkipList)
      根据实际场景选择适合的策略。
  6. 数据的时效性
    数据可能会变更,导致缓存失效。相应的,缓存有失效策略:
    • 过期失效
      设置过期时间,不使用过期数据,比如浏览器缓存、Redis
    • 缓存更新
      • Cache Aside 更新操作完成后,使缓存失效
      • Read/Write Through 缓存代理数据读写
      • Write Behind 异步批量更新数据库,性能高,逻辑复杂,牺牲强一致性

并发情况下可能出现的问题:

  1. 缓存并发
    防止并发未命中时重复建立缓存。
    思路: 缓存未命中时,tryLock(key),上锁成功的线程通过查询or计算更新缓存,否则block。
    java中可缓存future对象,防止重复运算。
  2. 缓存穿透
    防止反复查询不存在的值。可缓存空值
  3. 缓存批量过期导致雪崩

    锁解决的问题:保证并发情况下数据一致性。
    应用程序通常采用 CAS或锁机制保证数据一致性,CAS底层采用系统命令,硬件层锁内存总线。
    JVM通过Compare and Swap修改对象头中的锁状态实现加锁。
    数据库中将锁分为共享锁和互斥锁,实现读写分离。

池的作用:管理并重复利用资源

  1. 资源创建、销毁开销比较大
  2. 资源无限制的创建会导致系统崩溃
    常见的有:线程池、数据连接池

管道

负载均衡

java8 HashMap源码解读

逐行解读HashMap

先看put方法

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
56
57
58
59
60
61
62
63
64
65
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
//计算hash
static final int hash(Object key) {
int h;
//用key的hashCode值右移16位,与hashCode异或,原值不大于2的16次方的话,相当于按位取反
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
// table是map的内置数组,若为空就要初始化
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
//通过n-1&hash计算该hash值在tab中的下标hash%(n-1),若该位置数组元素为空,直接放入
tab[i] = newNode(hash, key, value, null);
else {
//若原位置不为空,则分3种情况
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//p.key与key相同,则把p的引用赋给e
e = p;
else if (p instanceof TreeNode)
//如果p是个树节点,遍历树,如果存在键为key的Node就返回,不存在就挂树上
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//把p视为链表,遍历之,大致同上
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
//链表长度>=8(可以改jvm参数)时,把链表重构成红黑树
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
//如果原值为空或 onlyIfAbsent=false 则替换成新值
e.value = value;
//后处理,LinkedHashMap用来把node移至末尾
afterNodeAccess(e);
//返回原值
return oldValue;
}
}
//如果是新插入节点
++modCount;
//size超过阈值
if (++size > threshold)
resize();
//节点插入后处理
afterNodeInsertion(evict);
return null;
}

整个put的过程如上,我们再来看下其中调用的几个方法,先看resize():

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
//返回node数组
final Node<K,V>[] resize() {
获取原数组大小
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//原阈值
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {//已经是最大值,只能扩容阈值
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if (//翻倍(newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//初始容量->最大容量/2之间,将容量&阈值翻倍
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // 原容量<=0,阈值>0时,将阈值作为新容量 initial capacity was placed in threshold
newCap = oldThr;
else { // 初始化 zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
//默认初始化,阈值=0.75*容量
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//根据新容量计算新阈值
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
//创建新的Node数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
//分三种情况,将旧数组中的数据按hash装到新数组中
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
//将原树上的节点按(hash&oldCap)==0划分到两棵新树上,若新树size<8,转成链表
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
//将原链表拆成2个新链表
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}

resize()的过程无疑非常耗费资源(时间&内存空间),那么什么情况下会触发resize呢?
查看代码,可以发现resize有7处引用,分布在put、compute等方法中,但是条件只有两个,tab数组为空或有新元素插入后map.size()>threshold。
为了避免频繁map频繁扩容,在创建Map应该评估数据量,并设置合理的初始容量,一般为2的n次方。

java8 HashMap中还加了红黑树实现,以及链表与红黑树相互转化的阈值,有兴趣的同学可以看下TreeNode代码。

iLink项目回顾

iLink综合前置是我初入职场参与的第一个项目,但是在项目的架构思路、业务复杂度、技术含量方面上依然可圈可点。

简介

首先,项目的定位是银行ESB系统,即银行的企业服务总线,兼具统合银行现有业务系统和快速开发新业务的能力。项目由三部分组成:

  • 核心运行平台:OSGI框架开发的后台系统,运行二次开发平台导出的业务包
  • 二次开发平台:RCP开发的银行业务开发工具,可视化编辑业务流,可自定义各种业务组件。(JBPM流程复用)
  • 运维监控平台:监控核心平台的运行,提供实时告警和业务报表等功能

使用Python做分词统计遇到的几个问题

背景

工作中需要对几百条问题原因进行分类汇总,分析出主要是哪些方面做的不到位,并进行针对性的改进。最开始想通过Excel统计,但是很繁琐,麻烦在于:

  1. 不确定需要统计哪些词句的频率
  2. 需要对输入数据进行整理,使同类原因有一个共性,才方便统计,比如”XXX不熟悉”、”YYY不了解”要统一改为”不了解”或”不熟悉”,才好统计

分词统计

以前看过相关文章,让我想到了分词统计可以解决这类问题,搜了下,找到这篇文章
然后就开始照搬,遇到了一系列的问题,记录如下:

  1. python2.7 安装pip20报错,查了下只能装pip9 (HUI框架需要装Python,令人费解)
  2. python2.7 不支持file.open传encoding,代码报错
    将文件编码转成GBK,代码去掉encoding参数
  3. SyntaxError: Non-ASCII character ‘\xe6’ in file
    python运行源码报错,在第一行加 # coding=UTF-8解决
  4. 无法调用Jieba库–module ‘jieba’ has no attribute ‘Icut’
    文件名是jieba.py,与import jieba冲突。改文件名解决。
    参考
  5. UnicodeEncodeError: ‘ascii’ codec can’t encode character in position 0: ordinal not in range(128)
    未查到具体原因,怀疑是python2的问题,决定升级python3。后来判断可能是终端只能输出Ascii码,而分词后要输出中文字符
    参考
  6. 下载Python官网安装包各种失败,连手机无线也慢如龟爬,找了个现成的33装了,发现装pip要setuptools,装setuptools要pip。还是要下载Python3.5以后版本,自带pip、setuptools,省心。
    一通搜索,终于在Bing上搜到到了淘宝镜像,才很快下载好。(遇到这种官网上下载慢的,要么找国内镜像,要么翻墙,省心省力。用百度搜简直就是翻垃圾堆,找了半天下载下来发现带毒。
  7. 装完Python37之后,抄来的程序终于运行成功(所以搞不清问题5到底是啥原因了,以后有空了再去用Python27研究一下。PS:八成不会)。发现统计的数量有问题,“考虑”出现过42次,但是只统计到27次。
    小改了下程序,排除“考虑”,包含‘考’的词还有“考虑不周”,出现15次,是分开统计的,bingo。

修改后的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# coding=UTF-8
import jieba

file = open("ois.txt", "r",encoding='utf-8')#此处需打开txt格式且编码为UTF-8的文本
txt = file.read()
words = jieba.lcut(txt) # 使用jieba进行分词,将文本分成词语列表
count = {}
for word in words: # 使用 for 循环遍历每个词语并统计个数
if len(word) < 2: # 排除单个字的干扰,使得输出结果为词语
continue
else:
count[word] = count.get(word, 0) + 1 #如果字典里键为 word 的值存在,则返回键的值并加一,如果不存在键word,则返回0再加上1

exclude = ["记得", "具体", "原因"] # 建立无关词语列表
for key in list(count.keys()): # 遍历字典的所有键,即所有word
if key in exclude:
del count[key] # 删除字典中键为无关词语的键值对
list = list(count.items()) # 将字典的所有键值对转化为列表
list.sort(key=lambda x: x[1], reverse=True) # 对列表按照词频从大到小的顺序排序
for i in range(5): # 此处统计排名前五的单词,所以range(5)
word, number = list[i]
print("关键字:{:-<10}频次:{:+>8}".format(word, number))

后续处理

分词统计出来的都是单词,比如:考虑、业务、设计等等,还需要把单词组成短语,然后将相同语义的短语归入一类进行统计,可以用NotePad++计数。语义统计和展示的方式就不再赘述。

观《天道》看授人以渔

看完《天道》之后,脑海中产生了很多感悟,却又纷乱复杂,没有头绪,难以诉诸语言和文字。
对于片中文化属性的提法还是雾里看花,无法琢磨。
偶然联想到古语中常说的“授人以鱼不如授人以渔”,不由的引发了对于王庙村扶贫究竟是授人以鱼还是授人以渔的思考。
丁元英指点欧阳雪买股票实际上是授人以鱼,虽然丁没有直接给鱼,而是告诉欧阳雪何时何处下网,何时捕捞。而王庙村呢,就很难判断,如果是授人以鱼,那王庙村的鱼是什么?如果是授人以渔,那渔又是什么?
如果是授人以鱼,我认为这个鱼就是丁提供的音响组装方案和格律诗公司+王庙村运作的架构方案,而通过专利申请和公司规范化管理的方式,这个鱼又可以看做渔,是王庙村脱贫的根本。
这又涉及到鱼与渔的分别。
鱼:资源
渔:获取资源的技能、方法
如果把钱作为资源,那丁元英一直在授人以渔,包括对指点欧阳雪买股票。
如果把能力作为资源,那丁元英对欧阳雪和王庙村就是授人以鱼,给与了他们一定时间内的赚钱的能力,买股票是一次性的,做音响相对而言是长期的。而这个层次的渔则是一种开悟,让人具备自己开发能力的意识和素质。

鱼渔之喻不得究竟,根本上还是道术之分。有道无术,术尚可求。有术无道止于术。然道却难传,需要自悟,而悟道的契机才是丁元英的礼物。