1. 第一个面试大概是年前还差半个月放假的某天晚上,一个电话面试,杭州的一个偏传统公司,应该是主要做 ERP 这类的系统,最后 HR 等年后通知就没通知了。

主要问了 java 集合,包括里面 List、Set、Map 各个区别以及相关算法、Spring 的设计模式、Spring MVC 和 Servlet 的联系区别、Spring 的相关源码,我当时只说了 Spring MVC 启动、运行源码流程,@Autowire 注入的时候的 AutowireAnnotationBeanPostProcessor 的相关逻辑,顺带讲了 Bean 的生命周期,循环注入等,还有一些常见的面试题给忘了。

2. 第二个面试是4.26号武汉小米的面试,以为一面不会过,没想到过了,可惜二面BOS没过,主要是对自己的项目没答好,可能由于没怎么复习以前的项目,不过学了很多。

一面先问 hashmap 的 put,currenthashmap 的 put,以及 size,然后是 ThreadLocal,订阅模式、命令模式、代理模式,sql,组合索引,手写一些 sql 问是否用到了索引,
手写一个 幻读,隔离级别与事务传播,那个幻读没写出来,然后被一面的面试官评价为基础可以但是 sql 不行,唉,然后问我最得意的项目,我说的是一个基于 ZooKeeper 的一个项目,然后问了 zk 的一些知识,两阶段提交、paxos(这个没答出来)、CAP、BASE,然后问如何实现一个分布式锁,然后问了项目的一些问题,涉及到了线程池。
二面直接让我把一个 BI 的项目技术架构图画出来,然后问 ETL ,RPC 和 http 的区别,然后其它一些小问题,跟我说要多敲代码,不是本专业只能要非常突出的代码能力才行。。。然后谈论了业务和技术,最后直接跟我说没过。比较奇怪没有问 JVM,可能是三面才会问这么高深的吧。。。
事后也把我做的项目都复习总结拿出来重新过了一遍,学了很多,然后好好复习,准备下一个面试。

3. 第三个面试是上海的一家创业公司,笔试,两轮技术,HR,拿到 offer 没去。

笔试:两个类A extend B,考察静态方法和构造方法的执行顺序,二分查找法,mysql 的 having order 等。
技术面试,一问 hashmap 然后问简历项目。最后问多个线程同时下载报表如何下载。主问简历。
二轮BOS,一问 某个业务从请求到最后日志记录的业务全过程。然后聊了很多,treemap 和 hashmap,git rebase merge,最后一个问题是一千个排好序的文件,如果成为一个大的排序好的文件。大概这么多

4. 第四个面试是上海同事推荐,两面技术,拿到 offer。

一面电话:集合、threadlocal、锁、线程池。多态、继承、封装的理解,主要是基础,具体问的大概忘了,并不深入。
二面聊天:一问,nginx 实现负载均衡的方式,底层如何实现负载均衡,zk 的实际案例,为什么用 zk,内部如何选举。大概这么多。

Jboot的入门demo —— JbooFly。

阅读全文 »

0 启动流程

  1. 解析启动参数,打印 logo。
  2. 通过工厂对配置进行判断获取相应的应用服务器(默认 undertow)。
  3. 判断是否是开发模式(默认),如果是则定期对文件进行扫描(3 * 1010)。
  4. 回调各个 listener 的 onJbootStarted() 方法。

1 如何使用 main 文件启动一个应用服务器?

如果你会使用,可以直接跳过这节。

pom.xml:

1
2
3
4
5
6
7
8
9
10
11
12
...
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<version>9.4.8.v20171121</version>
</dependency>

<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-servlet</artifactId>
<version>9.4.8.v20171121</version>
</dependency>

SimpleServer.java:

1
2
3
4
5
6
7
8
9
10
11
12
public class SimpleServer {

public static void main(String[] args) throws Exception {
InetSocketAddress address = new InetSocketAddress("0.0.0.0", 8081);
Server server = new Server(address);
ResourceHandler handler = new ResourceHandler();
handler.setDirectoriesListed(true);
handler.setResourceBase("/Users/liwenguang/Downloads");
server.setHandler(handler);
server.start();
}
}

参考资料:http://blog.csdn.net/kiterunner/article/details/51695293

2 Jboot 启动精简版

Jboot.java 主文件:

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
public class Jboot {

private JbootServer jbootServer;

public void start() {
ensureServerCreated();
if (!startServer()) {
System.err.println("jboot start fail!!!");
return;
}
}

private void ensureServerCreated() {
if (jbootServer == null) {
JbootServerFactory factory = JbootServerFactory.me();
jbootServer = factory.buildServer();
}
}

private boolean startServer() {
return jbootServer.start();
}

public static void main(String[] args) {
new Jboot().start();
}
}

JbootServer 抽象类,方便各种应用服务器的工厂创建,其中作者只编写了 undertow 和 jetty 的实现。(可知作者对 tomcat 不大喜欢):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public abstract class JbootServer {
public abstract boolean start();
public abstract boolean restart();
public abstract boolean stop();
}
```
JbootServerFactory 工厂类:
```java
public class JbootServerFactory {

private static JbootServerFactory me = new JbootServerFactory();
public static JbootServerFactory me() {
return me;
}
public JbootServer buildServer() {
// switch
return new JettyServer();
}
}

接着是应用服务器的配置文件, JbootServerConfig:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class JbootServerConfig {

public static final String TYPE_UNDERTOW = "undertow";
public static final String TYPE_TOMCAT = "tomcat";
public static final String TYPE_JETTY = "jetty";

private String type = TYPE_UNDERTOW;
private String host = "0.0.0.0";
private int port = 8080;
private String contextPath = "/";

// set/get 省略
}

最后是实现的 Jetty 应用服务器, JettyServer:

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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
public class JettyServer extends JbootServer {

private static Log log = Log.getLog(JettyServer.class);

private JbootServerConfig config;
// private JbootWebConfig webConfig;

private Server jettyServer;
private ServletContextHandler handler;

public JettyServer() {
config = new JbootServerConfig();
// webConfig = Jboot.config(JbootWebConfig.class);
}

@Override
public boolean start() {
try {
initJettyServer();
// JbootAppListenerManager.me().onAppStartBefore(this);
jettyServer.start();
} catch (Throwable ex) {
log.error(ex.toString(), ex);
stop();
return false;
}
return true;
}

private void initJettyServer() {
InetSocketAddress address = new InetSocketAddress(config.getHost(), config.getPort());
jettyServer = new Server(address);

handler = new ServletContextHandler();
handler.setContextPath(config.getContextPath());
handler.setClassLoader(new JbootServerClassloader(JettyServer.class.getClassLoader()));
handler.setResourceBase(getRootClassPath());
/*
增加 shiro 全局过滤器
JbootShiroConfig shiroConfig = Jboot.config(JbootShiroConfig.class);
if (shiroConfig.isConfigOK()) {
handler.addEventListener(new EnvironmentLoaderListener());
handler.addFilter(ShiroFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
}
*/
/*
增加 Jfinal Handler,Jboot 基于 Jfinal
//JFinal
FilterHolder jfinalFilter = handler.addFilter(JFinalFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
jfinalFilter.setInitParameter("configClass", Jboot.me().getJbootConfig().getJfinalConfig());
增加 Hystrix 监控 servlet
JbootHystrixConfig hystrixConfig = Jboot.config(JbootHystrixConfig.class);
if (StringUtils.isNotBlank(hystrixConfig.getUrl())) {
handler.addServlet(HystrixMetricsStreamServlet.class, hystrixConfig.getUrl());
}

增加 metric 监控
JbootMetricConfig metricsConfig = Jboot.config(JbootMetricConfig.class);
if (StringUtils.isNotBlank(metricsConfig.getUrl())) {
handler.addEventListener(new JbootMetricServletContextListener());
handler.addEventListener(new JbootHealthCheckServletContextListener());
handler.addServlet(AdminServlet.class, metricsConfig.getUrl());
}
最后增加 Jboot 本身的 servlet
io.jboot.server.Servlets jbootServlets = new io.jboot.server.Servlets();
ContextListeners listeners = new ContextListeners();
JbootAppListenerManager.me().onJbootDeploy(jbootServlets, listeners);
for (Map.Entry<String, io.jboot.server.Servlets.ServletInfo> entry : jbootServlets.getServlets().entrySet()) {
for (String path : entry.getValue().getUrlMapping()) {
handler.addServlet(entry.getValue().getServletClass(), path);
}
}
事件监听
for (Class<? extends ServletContextListener> listenerClass : listeners.getListeners()) {
handler.addEventListener(ClassKits.newInstance(listenerClass));
}

*/
jettyServer.setHandler(handler);
}

private static String getRootClassPath() {
String path = null;
try {
path = JettyServer.class.getClassLoader().getResource("").toURI().getPath();
return new File(path).getAbsolutePath();
} catch (URISyntaxException e) {
e.printStackTrace();
}
return path;
}

@Override
public boolean restart() {
stop();
start();
return true;
}

@Override
public boolean stop() {
try {
jettyServer.stop();
return true;
} catch (Exception ex) {
log.error(ex.toString(), ex);
}
return false;
}
}

最后是自定义 ClassLoader,JbootServerClassLoader:

1
2
3
4
5
6
7
8
9
10
11
12
public class JbootServerClassloader extends ClassLoader {

public JbootServerClassloader(ClassLoader parent) {
super(parent);
}


@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
}

自定义 ClassLoader 在应用服务器中都会自定义,用于文件的隔离和热更新。

目录结构如下:

3 启动到底启动了什么

1. 参数解析

类似 JVM options 的 -Dxxx=xxx 参数的作用,用于全局访问,Jboot 将启动参数使用 Jboot.setBootArg() 放在了一个 Map 中,你可以使用 Jboot.getBootArg() 获取。
Jboot.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private static void parseArgs(String[] args) {
if (args == null || args.length == 0) {
return;
}

for (String arg : args) {
int indexOf = arg.indexOf("=");
if (arg.startsWith("--") && indexOf > 0) {
String key = arg.substring(2, indexOf);
String value = arg.substring(indexOf + 1);
setBootArg(key, value);
}
}
}

2. 判断启动模式

默认为 dev 模式,查看 JbootConfig.java 文件可知,但是我们可能为想,我们怎么才能设置启动模式呢?

没错,使用启动参数!请看 JbootConfigManager 文件,该文件是用于读取配置文件,你可能会想,为什么配置文件都加了 @PropertyConfig(prefix = "") 这样的注解呢,其实,这是作者为了方便 JavaBean 与 参数 进行转换。直接上代码:

第一种:启动参数,如下图:

我们配置了两个参数(对照 JbootConfig 你就知道,只有 mode 有 set 方法,而 version 是只有 get 方法的)。
最后启动 debug 的时候你就会发现 Jboot.isDevMode() 方法返回 false 而不是默认的 true。

有很多地方判断了,如果是 dev 模式,则会打印一些参数,例如 JbootEventManager 方法。

第二种:使用 Jboot.setBootArg("jboot.mode", "test"); 这种,从前面的 参数解析 一节我们已经知道,其实启动参数底层使用的就是 setBootArg 方法。

测试类中很多使用了这种方法,例如 DubboClientZookeeperDemo

如果是 dev 模式,就会定时 3 秒扫描应用服务器文件夹,但是作者注释了,这里不懂作者的意思。
AutoDeployManager.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void run() {

File file = new File(PathKit.getRootClassPath());
JbootFileScanner scanner = new JbootFileScanner(file.getAbsolutePath(), 3) {
@Override
public void onChange(String action, String file) {
try {
// System.err.println("file changes : " + file);
// Jboot.me().getServer().restart();
// JbootServerFactory.me().buildServer().start();
// System.err.println("Loading complete.");
} catch (Exception e) {
System.err.println("Error reconfiguring/restarting webapp after change in watched files");
LogKit.error(e.getMessage(), e);
}
}
};

scanner.start();
}

3. 回调所有 JbootAppListener 实现类的 onJbootStarted()方法

在 Jboot 启动的最后一步,实例化了 JbootAppListenerManager 类:

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
 private JbootAppListenerManager() {
// 扫描获取所有 JbootAppListener 的子类
List<Class<JbootAppListener>> allListeners = ClassScanner.scanSubClass(JbootAppListener.class, true);
if (allListeners == null || allListeners.size() == 0) {
return;
}
// 去除 JbootAppListenerManager 本身
for (Class<? extends JbootAppListener> clazz : allListeners) {
if (JbootAppListenerManager.class == clazz || JbootAppListenerBase.class == clazz) {
continue;
}
// 实例化
JbootAppListener listener = ClassKits.newInstance(clazz, false);
if (listener != null) {
listeners.add(listener);
}
}
}

@Override
public void onJbootStarted() {
for (JbootAppListener listener : listeners) {
try {
listener.onJbootStarted();
} catch (Throwable ex) {
log.error(ex.toString(), ex);
}
}
}

并通过 JbootAppListenerManager.me().onJbootStarted(); 回调了 onJbootStarted() 方法,来调用用户的逻辑。

4 其它

  1. 从一些 Manager 方法看的出作者习惯通过构造方法进行一些必要的初始化,我以前看的 《架构探险——从零开始写Java Web框架》 则喜欢用静态块进行初始化。
  2. 启动的一些细节需要大家去 debug 一步一步看,看懂了也是很高兴的,毕竟作者也是大牛,更近了一步。
  3. 作者代码习惯方法名由于注释。说实话初看有点不习惯,因为习惯看注释了,但是作者方法名真的能让你可以不用注释(除却一些必要方法作者加了注释)。
  4. jbootfly 是入门,不要想直接看源码,欲速则不达。
  5. 你要懂 jfinal 的知识,至少看过 jfinal 文档,写过 jfinal 经典的 blog 项目。

如果有错误,请指出,谢谢,共勉。

除非你觉得你的时间不是很宝贵,否则不要看这篇流水账式的博文,这只是篇个人的工作的学习一个总结而已,没有包含任何的技术细节
阅读全文 »

索引能够轻易将查询性能提高几个数量级,“最优”的索引有时比一个“好的”索引性能要好两个数量级。创建一个真正“最优”的索引经常需要重写查询。

1 索引基础

1.1 索引的类型

在 MySQL 中,索引是在存储引擎层而不是服务器层实现的。所以,并没有统一的索引标准。即使多个存储引擎支持同一种类型的索引,其底层的事先也可能不同。

1. B-Tree 索引

InnoDB 本质是“B+Tree”。假设有如下数据表:

1
2
3
4
5
6
7
CREATE TABLE People(
last_name varchar(50) not null,
first_name varchar(50) not null,
dob date not null,
gender enum('m', 'f') not null,
key(last_name, first_name, dob)
)

对于表中的每一行数据,索引中包含了 last_name、first_name和 dob 列的值,下图显示了该索引是如何组织数据的存储的:

注:索引对多个值进行排序的依据是CREATE TABLE语句中定义索引时列的顺序。

  1. 全值匹配:指和索引中的所有列进行匹配。
  2. 匹配最左前缀:前面提到的索引可用于查询所有姓为 Allen 的人,即只使用索引的第一列。
  3. 匹配列前缀:例如查找所有以 J 开头的姓的人。这里也只使用了索引的第一列。
  4. 匹配范围值:例如查找姓在 Allen 和 Barrymore 之间的人。
  5. 精确匹配某一列并范围匹配另外一列:查找所有姓为 Allen,并且名字是字母 K 开头的人。即第一列 last_name 全匹配,第二列 first_name 范围匹配。
  6. 只访问索引的查询

因为索引树的节点是有序的,所以除了按值查找之外,索引还可以用于查询中的ORDER BY操作(按顺序查找)。一般来说,如果 B-Tree 可以按照某种方式查找到值,那么也可以按照这种方式用于排序。所以如果ORDER BY子句满足前面列出的几种查询类型,则这个索引也可以满足对应的排序需求。

下面是一些关于 B-Tree 索引的限制:

  • 如果不是按照索引的最左列开始查找,则无法使用索引。例,上面的例子的索引无法查找名字为 Bill 的人,也无法查找某个特定生日的人,因为这两列都不是最左数据列。类似地,也无法查找姓氏以某个字母结尾的人。这是关于like '%关键字'的知乎讨论:https://www.zhihu.com/question/52718330?sort=created
  • 不能跳过索引中的列。
  • 如果查询中有某个列的范围查询,则其右边所有列都无法使用索引优化查询。

有些限制并不是 B-Tree 本身导致的,而是 MySQL 优化器和存储引擎使用索引的方式导致的,这部分限制在未来的版本中可能就不再是限制了。

2. 哈希索引

哈希索引(hash index)基于哈希表实现,只有精确匹配索引所有列的查询才有效。对于每一行数据,存储引擎都会对所有的索引列计算一个哈希码(hash code),哈希码是一个较小的值,并且不同键值的行计算出来的哈希码也不一样。哈希索引将所有的哈希码存储在索引中,同时在哈希表中保存指向每个数据行的指针。

  1. 哈希索引只包含哈希值和行指针,而不存储字段值,所以不能使用索引中的值来避免读取行。不过,访问内存中的行的速度很快,所以大部分情况这一点对性能的影响并不明显。
  2. 哈希索引数据并不是按照索引值顺序存储的,所以也就无法用于排序。
  3. 哈希索引也不支持部分索引列匹配查找,因为哈希索引始终是使用索引列的全部内容来计算哈希值的。例,在(A,B)建立哈希索引,如果查询只有 A ,则无法使用该索引。
  4. 访问哈希索引的数据非常快,除非有很多哈希冲突。当出现哈希冲突的时候,存储引擎必须遍历链表中所有的行指针。

当字段过大,例如存储 URL 时,可以额外使用一个列,用于存储该列的哈希,用于排序。但为了避免哈希冲突的问题,需要where url = 'http://www.mysql.com' AND url_crc=CRC32('http://www.mysql.com'),这样的性能会非常高。(可以使用触发器来自身插入哈希列)。

3. 空间数据索引(R-Tree)

MyISAM 表支持空间索引。

4. 全文索引

使用MATCH AGAINST操作,而不是普通的WHERE

2 索引的优点

最常见的 B-Tree 索引,按照顺序存储数据,所以 MySQL 可以用来做 ORDER BYGROUP BY 操作。因为数据时有序的,所以 B-Tree 也就会将相关的列值都存储在一起。最后,因为索引中存储了实际的列值,所以某些查询只使用索引就能够完成全部查询:

  1. 索引大大减少了服务器需要扫描的数据量。
  2. 索引可以帮助服务器避免排序和临时表。
  3. 索引可以将随机 I/O 变为顺序 I/O。

对于中到大型表,索引就非常有效。但对于特大型表,需要技术直接区分出查询需要的一组数据,而不是一条记录一条记录地匹配,例,分区技术:建立元数据信息表,例,假如执行那些需要聚合多个应用分布在多个表的数据的查询,则需要记录“哪个用户的信息存储在哪个表中”的元数据,这样在查询时就可以直接忽略那些不包含指定用户信息的表。对于 TB 级别的数据,定位单条记录的意义不大,所以经常会使用块级别元数据技术来替代索引。

3 高性能索引的索引策略

3.1 前缀索引和索引选择性

索引选择性:不重复的索引值/数据表的记录总数,范围在0~1间,越高则查询越快。唯一索引的选择性是1,性能也是最好的。
列值过长,可以使用部分字段LEFT(city,3)作为前缀索引,这个值是通过COUNT(DISTINCT LEFT(city,5))/COUNT(*)的值来决定的,结果在 0.031 基本上就可用了。接着创建:ALTER TABLE sakila.city_demo ADD KEY(city(5));
但是前缀索引虽然使索引更小、更快,但是无法使用前缀索引做ORDER BYGROUP BY,也无法使用前缀索引做覆盖扫描。

后缀索引(suffix index)也很有用途(电子邮箱),可以使用字符串反转后存储。

3.2 多列索引

为每个列创建独立的索引,从SHOW CREATE TABLE中很容易看到这种情况:

1
2
3
4
5
6
7
8
CREATE TABLE t(
c1 INT,
c2 INT,
c3 INT,
key(c1),
key(c2),
key(c3)
)

这种索引策略是由于“把 WHERE 条件里面的列都建上索引”这样模糊的建议导致的。这最多是“一星”索引。例,WHERE c1 = '1' OR c2 = '2',虽然在 MySQL5.0+ 会优化使用这两个索引,但是在 explainextra 中会有 Using union,因此说明了索引的糟糕:

  1. 出现多个 AND 条件,则需要一个包含所有相关列的多列索引,而不是多个独立的单列索引。
  2. 多个 OR 或者联合操作时,如果有些索引的选择性不高,需要合并扫描返回的大量数据。
  3. 如果在 EXPLAIN 中看到有索引合并,应该好好检查查询和表的结构,也可以通过参数 optimizer_switch 来关闭索引合并功能,也可以使用 IGNORE INDEX 提示让优化器忽略掉某些索引。

3.3 选择合适的索引列顺序

最好通过 pt-query-digest 这样的工具报告重提取“最差”查询,再按照索引顺序进行优化。如果没有类似的具体查询来运行,那么最好还是按照经验法则来做,因为经验法则考虑的是全局性和选择性,而不是某个具体查询:

customer_id 的选择性更高,所以答案是将其作为索引列的第一列。

3.4 覆盖索引

如果一个索引包含(覆盖)所有需要查询的字段的值,我们就称为“覆盖索引”。

  1. 因为索引是按照列值顺序存储的,所以对于 I/O 密集型的范围查询会比随机从磁盘读取每一行数据的 I/O 要少得多。
  2. 并不是所有类型的索引都可以称为覆盖索引。覆盖索引必须要存储索引列的值。

当发起一个被覆盖的查询,在 EXPLAINExtra 列可以看到 Using index 的信息。

type 列的 indexExtra 列的 Using index 是完全不同,前者和覆盖索引毫无关系,它只是表示这个查询访问数据的方式。

索引覆盖查询还有很多陷阱可能导致无法实现优化:

  1. 没有任何索引能够覆盖这个查询。因为查询从表中选择了所有的列,而没有任何索引覆盖了所有的列。
  2. MySQL 不能再索引中执行 LIKE 操作,MySQL5.5- 只允许在索引中做简单比较操作,MySQL 能在索引中做最左前缀匹配的 LIKE 查询,因为该操作可以转为简单的比较操作,但是如果是通配符开头的 LIKE 查询,存储引擎就无法做比较匹配。这种情况下,MySQL 服务器只能提取数据行的值,而不是索引值来做比较。

也有办法解决上面说的两个问题,重写查询并巧妙地设计索引。先将索引扩展至三个数据列(artist,title,prod_id),然后按如下方式重写查询:

3.5 使用索引扫描来做排序

MySQL 有两种方式可以生成有序的结果:通过排序操作;或按索引顺序扫描;如果 EXPLAIN 出来的 type 列的值为 “index”,则说明 MySQL 使用了索引扫描来做排序(不要和 Extra 列的 “Using index” 搞混淆了)。
扫描索引本身是很快的,因为只需要从一条索引记录移动到紧接着的下一条记录。但如果索引不能覆盖查询所需的全部列,那就不得不每扫描一条索引记录就回表查询一次对应的行。这基本上都是随机 I/O,因此按索引顺序读取数据的速度通常要比顺序地全表扫描慢。
如果查询需要关联多张表,则只有当 ORDER BY 子句引用的字段全部为第一个表时,才能使用索引做排序。

例:

1
2
3
4
5
6
7
8
CREATE TABLE rental(
PRIMARY KEY(rental_id),
UNIQUE KEY rental_date(rental_date, inventory_id, customer_id),
KEY idx_fk_inventory_id(inventory_id),
KEY idx_fk_customer_id(customer_id),
KEY idx_fk_staff_id(staff_id),
...
)

WHERE rental_date = '2015-05-25' ORDER BY inventory_id, customer_id 因为索引第一列被指定为一个常数,所以查询排序。
WHERE rental_date = '2015-05-25' ORDER BY inventory_id 也可以使用查询排序。
WHERE rental_date > '2005-05-25 ORDER BY rental_date, inventory_id 也可以。
下面是不能使用索引做排序的查询:

  1. WHERE rental_date = '2015-05-25' ORDER BY inventory_id DESC, customer_id ASC; 因为索引列都是正序排序。
  2. WHERE rental_date = '2015-05-25' ORDER BY inventory_id, staff_id; 因为引用了一个不再索引中的列。
  3. WHERE rental_date = '2015-05-25' ORDER BY customer_id; 无法组成索引的最左前缀。
  4. WHERE rental_date > '2015-05-25' ORDER BY inventory_id, customer_id; 因为第一列上是范围条件。
  5. WHERE rental_date = '2015-05-25' AND inventory_id IN (1,2) ORDER BY customer_id; 还是范围查询。

4 索引案例学习

5 维护索引和表

6 总结

1 选择优化的数据类型

  1. 更小的通常更好
  2. 简单就好:整型比字符操作代价更低,使用 MySQL 内建的类型而不是字符串来存储日期和时间,以及使用整型存储 IP 地址
  3. 尽量避免 NULL:可为 NULL 的列会使用更多的存储空间。 InnoDB 使用单独的位(bit)存储 NULL 值,但这不适用于 MyISAM

在为列选择数据类型时,先确定大类型:数字、字符串、时间等。下一步是选择具体类型,很多数据类型可以存储相同类型的数据,只是存储的长度和范围不一样、允许的精度不同,或者需要的物理空间(磁盘和内存空间)不同。例如, TIMESTAMP 只使用 DATETIME 一半的存储空间,并且会根据时区变化,具有特殊的自动更新能力,另一方面, TIMESTAMP 允许的时间范围要小得多。

本章只讨论基本的数据类型。 MySQL 为了兼容性支持很多别名,例如 INTEGER、BOOL 以及 NUMERIC ,它们只是别名,使用 SHOW CREATE TABLE 检查, MYSQL 报告的是基本类型,而不是别名。

1.1 整数类型

TINYINT,SMALLINT,MEDIUMINT,INT,BIGINT。分别使用8、16、24、32、64 位存储空间。它们可以存储的值得范围 -2^(N-1) ~ 2^(N-1) - 1,其中 N 是存储空间的位数。整数类型有可选的 UNSIGNED 属性,表示不允许负数,这样可以提高一倍的正数上限。

整数计算一般使用 64 位的 BIGINT 整数。整数类型指定宽度,例如 INT(11),对大多数应用这是没有意义的:它不会限制值得合法范围,只是规定了 MySQL 的一些交互工具(例如 MySQL命令行客户端)用来显示字符的个数。对于存储和计算来说,INT(1) 和 INT(20) 是相同的。

1.2 实数类型

实数是带有小数部分的数字。然而,它们不只是为了存储小数部分,也可以使用 DECIMAL 存储比 BIGINT 还大的整数。 MySQL 既支持精确类型,也不支持不精确类型。
因为 CPU 不支持对 DECIMAL 的直接计算,所以在 MySQL5.0+ MySQL 服务器自身实现了 DECIMAL 的高精度计算,相对而言,CPU 直接支持原生浮点计算,所以浮点运算明显更快。
DECIMAL 的字节存储:每四个字节存储 9 个数字,例:DECIMAL(18,9) 小数点两边将各存储 9 个数字,一共使用 9 个字节:小数点前的数字用 4 个字节,小数点后的数字用 4 个字节,小数点本身占 1 个字节。
浮点类型在存储同样范围的值时,通常比 DECIMAL 使用更少的空间。 FLOAT 使用 4 个字节存储。 DOUBLE 占用 8 个字节。MySQL 使用 DOUBLE 作为内部浮点计算的类型。
将结果存储在 BIGINT 里,这样可以同时避免浮点存储计算不精确和 DECIMAL 精确计算代价高的问题。(根据小数的位数乘以相应的倍数)

1.3 字符串类型

1.VARCHAR 和 CHAR 类型

VARCHAR

VARCHAR 类型用于存储可变长字符串,如果 MySQL 表使用 ROW_ FORMAT = FIXED 创建的话,每一行都会使用定长存储,这会很浪费空间。
VARCHAR 在列最大长度 <=255 字节的时候,额外用 1 个字节用于记录字符串的长度。 例:VARCHAR(10) 的列需要 11 个字节的存储空间。VARCHAR(1000) 的列则需要 1002 个字节,因为需要 2 个字节存储长度信息。
MySQL5.0+ 在存储和检索时会保留末尾空格。
但是,由于行是变长的,在 UPDATE 时可能使行变得比原来长,这就导致需要额外的工作。
另外,InnoDB 可以把过长的 VARCHAR 存储为 BLOB,稍后讨论该问题。

CHAR

CHAR 类型是定长的,MySQL 总是根据定义的字符串长度分配足够的空间。
存储 CHAR 值时,MySQL 会删除所有的末尾空格。

CHAR VS VARCHAR

CHAR 适合存储很短的字符串,或者所有值都接近同一个长度。例:存储密码的 MD5 值,因为这是一个定长的值。
对于经常变更的数据, CHAR 也比 VARCHAR 更好,因为定长的 CHAR 类型不容易产生碎片。
对于非常短的值, CHAR(1) 比 VARCHAR(1) 在存储空间上也更有效率(后者需要额外一个字节存储长度)。
VARCHAR(100) 和 VARCHAR(200) 虽然在存储空间相同,但是在内存消耗不同,后者更大。尤其在排序和临时表(中间表)时。
摘自:http://tech.it168.com/a2011/0426/1183/000001183173.shtml

2. BLOB 和 TEXT 类型

BLOB 采用二进制存储、TEXT 采用字符存储。
与其它类型不同,MySQL 把每个 BLOB 和 TEXT 值当做一个独立的对象处理。当其太大时, InnoDB 会使用专门的“外部”存储区域进行存储。此时每个值在行内需要 1~4 个字节存储一个指针,然后再外部存储区域实际的值。
排序:MySQL 只对每个列的最前 max_sort_length 字节而不是整个字符串做排序。可以减少 max_sort_length 的值或者使用 ORDER BY SUBSTRING(column, length)。
MySQL 不能讲 BLOB 和 TEXT 列全部长度的字符串进行索引,也不能使用这些索引消除排序。
进行 ORDER BY 为了防止临时表过大,可以使用 SUBSTRING(column, length) 进行长度切割。

3. 使用枚举(ENUM) 代替字符串类型

MySQL 在存储枚举时非常紧凑,会根据列表值得数量压缩到一个或者两个字节中。 MySQL 在内部会将每个值在列表中的位置保存为整数,并且在表的 .frm 文件中保存 “数字-字符串”映射关系的“查找表”。
在 VARCHAR 与 ENUM 互相 JOIN 关联时,ENUM 与 ENUM 最快。因此如果不是必须和 VARCHAR 列进行关联,那么转换这些列为 ENUM 就是个好主意。这是一个通用的设计实践,在“查找表”时采用整数主键而避免采用基于字符串的值进行关联。

1.4 日期和时间类型

1.DATETIME

这个类型能保存大范围的值,精度为秒。使用 8 个字节的存储空间。

2.TIMESTAMP

保存了从 1970年1月1日~2038年,MySQL 提供了 FROM_UNIXTIME() 和 UNIX_TIMESTAMP() 函数将日期和 Unix 时间戳转换。使用 4 个字节存储。

1.5 位数据类型

1.BIT

尽量少用。

2.SET

如果需要保存很多 true/false 值,可以考虑合并这些列到一个 SET 数据类型,它在 MySQL 内部是以一系列打包的位的集合来表示的。这样就有效的利用了存储空间。缺点是改变列的定义代价较高:需要 ALTER TABLE(这对大表是非常昂贵的操作,但是后面给出了解决方法)。一般来说,也无法再 SET 列上通过索引查找。

在整数列进行按位操作

1
2
3
4
5
6
7
8
9
>SET @CAN_READ   := 1 << 0,
> @CAN_WRITE := 1 << 1,
> @CAN_DELETE := 1 << 2;
>CREATE TABLE acl (
> perms TINYINT UNSIGNED NOT NULL DEFAULT 0
>);
>INSERT INTO acl(perms) VALUES (@CAN_READ+@CAN_DELETE);
>SELECT perms FROM acl WHERE perms & @CAN_READ;
>

当然,也可以使用代码变量而不是 MySQL 变量。

1.6 选择标识符(identifier)

标识列与其它值进行比较(例,在关联操作中),或通过标识列寻找其它列。标识列也可能在另外的表中作为外键使用。
选择标识列的类型时,不仅仅需要考虑存储类型,还需要考虑 MySQL 对这种类型怎么执行计算和比较。例, MySQL 在内部使用整数存储 ENUM 和 SET 类型,然后在做比较操作时转换为字符串。
在可以满足值得范围的需求,并且预留未来增长空间的前提下,应该选择最小的数据类型。例如,TINYINT 比 INT 少了 3 个字节,但是可能导致很大的性能差异。
尽量使用整数。如果存储 UUID 值,用 UNHEX() 函数转换为 16 字节的数字存储,并且存储在一个 BINARY(16) 列中。

1.7 特殊类型数据

例,IPv4 地址人们通常使用 VARCHAR(15) 列来存储 IP 地址。然而,它们实际上是 32 位无符号整数,不是字符串。所以应该用无符号整数存储 IP 地址。 MySQL 提供 INET_ATON() 和 INET_NTOA() 函数在这两种表示方法之间转换。

2 MySQL schema 设计中的陷阱

2.1 太多的列

MySQL 的存储引擎 API 工作时需要在服务器层和存储引擎层之间通过行缓冲格式拷贝数据,然后再服务器层将缓冲内容解码成各个列。列转行的操作代价是非常高的。

2.2 太多的关联

阿里手册规定单次关联不能超过 3 张表。

2.3 全能的枚举

CREATE TABLE … ( country enum(‘’, ‘0’, ‘1’, … , ‘31’))
当需要在枚举列表中增加一个新的国家时就要做一次 ALTER TABLE 操作,在 MySQL5.0- 这是一种阻塞操作,即使在 MySQL5.0+ ,如果不是在列表的末尾增加值也会一样需要 ALTER TABLE。

2.4 变相的枚举

枚举列允许在列中存储一组定义值中的单个值,集合(SET)列则允许在列中存储一组定义值的一个或多个值。这会导致混乱。

2.5 非此发明(Not Invent Here)的 NULL

CREATE TABLE … (dt DATETIME NOT NULL DEFAULT ‘0000-00-00 00:00:00’)
伪造的全 0 值可能导致很多问题。(可以配置 MySQL 的 SQL_MODE 来禁止不可能的日期,对于新应用这是个非常好的实践经验)。

3 缓存表和汇总表

有时提升性能最好的方法是在同一张表中保存衍生的冗余数据。然而,有时也需要创建一张完全独立的汇总表或缓存表。

3.1 计数器表

创建一张独立的表存储计数器通常是个好主意。例,有一个计数器表,只有一行数据,记录网站的点击次数:

1
2
3
CREATE TABLE hit_counter (
cnt int unsigned not null
) engine=InnoDB;

每次点击:UPDATE hit_counter SET cnt = cnt + 1;
问题在于,对于任何想要更新这一行的事务来说,这条记录上都有一个全局的互斥锁(mutex)。这会使得这些事务只能串行执行。要获得更高的并发更新性能,也可以将计数器保存在多行中,每次随机选择一行进行更新。
要获得统计结果:SELECT SUM(cnt) FROM hit_count;
一个常见的需求是每隔一段时间开始一个新的计数器(例,每天一个)。如果需要这么做,则可以再简单地修改一下表设计:

1
2
3
4
5
6
CREATE TABLE hit_counter (
day date not null,
slot tinyint unsigned not null,
cnt int unsigned not null,
primary key(day, slot)
) ENGINE=InnoDB;

在这个场景下,可以不用像前面的例子那样预先生成行,而是ON DUPLICATE KEY UPDATE代替。

1
2
3
INSERT INTO daily_hit_counter(day, slot, cnt)
VALUES (CURRENT_DATE, RAND() * 100, 1)
ON DUPLICATE KEY UPDATE cnt = cnt + 1;

如果希望减少表的行数,以避免表变得太大,可以写一个周期执行的任务,合并所有结果到 0 号槽,并删除所有其它的槽:

1
2
3
4
5
6
7
8
9
UPDATE daily_hit_counter as c
INNER JOIN (
SELECT day, SUM(cnt) AS cnt, MIN(slot) AS mslot
FROM daily_hit_counter
GROUP BY day
) AS x USING(day)
SET c.cnt = IF(c.slot = x.mslot, x.cnt, 0),
c.slot = IF(c.slot = x.mslot, 0, c.slot);
DELETE FROM daily_hit_counter WHERE slot <> 0 AND cnt = 0;

4 加快 ALTER TABLE 操作的速度

假如要修改电影的默认租赁期限,从三天改到五天,下面是很慢的方式:

1
2
ALTER TABLE film 
MODIFY COLUMN rental_duration tinyint(3) not null default 5;

show status语句显示这个语句做了 1000 次读和 1000 次插入操作。换句话说,它拷贝了整张表到一张新表。
理论上,MySQL 可以跳过创建新表的步骤,即直接修改 .frm 文件而不设计表数据:

1
2
ALTER TABLE film
ALTER COLUMN rental_duraion SET DEFAULT 5;

5 总结

  1. 避免过度设计
  2. 使用小而简单的合适数据类型,避免使用 NULL 值
  3. 关联标识符尽量使用相同的数据类型
  4. 注意可变长字符串,其在临时表和排序时可能导致悲观的按最大长度分配内存
  5. 尽量使用整型定义标识列
  6. 小心使用 ENUM 和 SET
  7. ALTER TABLE在大部分情况下都会锁表并且重建整张表。建议先在备库执行ALTER完成后将其切换为主库

在《架构探险——从零开始架构》中,第四章的自己实现 ThreadLocal 感悟:
ThreadLocal 中虽然使用了 Map 进行保存线程变量,但是为了防止引入锁(Map 的多线程访问)影响性能,从而使用让不同的 Thread 保存不同的 Map(ThreadLoaclMap)实例,这样不同的Thread 有不同的 ThreadLocalMap 实例,就不用考虑锁的问题。
另外为了避免内存泄漏、回收不及时等问题,从而让 ThreadLocalMap 的 key 使用弱引用。
同时,为了保证当 key 为 null 时,value 无法正常释放时,在每次 set 时,都会遍历 key ,当 key 为 null 则会执行 replaceStaleEntry(),即将 key 为 null 的 value 值也置为 null,从而来让其回收。
这里讲解更加详细:http://www.jasongj.com/java/threadlocal/
这是原理:

笔记总结+源码:https://github.com/LiWenGu/MySourceCode/tree/master/mybatis0to1
在前七章都打了对应的标签,可以通过 git checkout来。

总结就是,将 sql 语句从代码中抽离出来,通过 xml 的配置来实现单表、多表的映射,最后通过动态代理来执行方法,很强的解耦性。
把 SQL 放在了 XML 中,然后用一些判断来实现动态 SQL ,最后通过 SqlSession 、SqlSessionFacotry 的生命周期来绑定一级、二级缓存。
不学之前感觉很神奇,学完之后也就那么回事,不过还是要多学学基础,例如读取配置、缓存、一级动态代理等。

Please look after this bear, thank you.

联想最近的北京大兴事件,有点苦涩,现代人的城市,忘记如何对待一个流浪的人。

每个城市都会有着提着行李箱,下着雨,在站台上的外乡人,唯一的区别是如何对待他们。


城市应该有它自己的包容心。

1 本章概要

本章中,将以密码软件 PGP(Pretty Good Privacy)为题材,思考一下将前面章节中学习的密码技术进行组合的方法。

2 PGP简介

PGP 是 1990 年编写的密码软件。

2.1 PGP 的功能

1.对称密码

2.公钥密码

3.数字签名

4.单向散列函数

5.证书

6.压缩

7.文本数据

8.大文件的拆分和平和

9.钥匙串管理

3 生成秘钥对

4 加密与解密

5 生成和验证数字签名

6 生成数字签名并加密以及解密并验证数字签名

7 信任网

7.1 公钥合法性

在使用 PGP 时,确认自己所得到的公钥是否真的属于正确的人(公钥合法性)是非常重要的,因为公钥可能会通过中间人攻击被替换(参 5.7.4节)。
第十章介绍的证书就是确认公钥合法性的方法之一。证书就是由认证机构对公钥所施加的数字签名,通过验证这个数字签名就可以确认公钥合法性。
然而,PGP 中却没有使用认证机构,而是采用了一种叫信任网(web of trust)的方法。在这种方法中, PGP 用户会互相对对方的公钥进行数字签名
信任网的要点是“不依赖认证机构,而是建立每个人之间的信任关系”。换言之,就是能够自己决定要信任哪些公钥。
下面介绍 PGP 的信任网是如何建立起来的。

1.场景1:通过自己的数字签名进行确认

Alice 和 Bob 约会,在告别的时候, Bob 给了 Alice 一张存储卡,并说“这是我的公钥”。
Alice 回到家中,从存储卡中取出 Bob 的公钥,并存放到自己所使用的 PGP 的公钥串中(导入公钥)。由于 Alice 确信刚刚导入的公钥确实是属于 Bob 本人的,因此 Alice 对这个公钥加上了自己的数字签名。
对 Bob 的公钥加上数字签名,就相当于 Alice 生命“这个公钥属于 Bob 本人(即这个公钥是合法的)”。
随后,Alice 收到了来自 Bob 的邮件,由于这封邮件带有 Bob 的数字签名,因此 Alice 想用 PGP 来验证 Bob 的数字签名。 PGP 将执行下面这些操作:

  1. 为了验证 Bob 的数字签名, PGP 需要从 Alice 的公钥串中寻找 Bob 的公钥。
  2. Alice 的公钥串中包含 Bob 的公钥,因为前几天约会之后 Alice 导入了 Bob 的公钥。
  3. PGP 发现 Bob 的公钥带有 Alice 的数字签名。
  4. 为了验证 Alice 的数字签名,PGP 需要从 Alice 的公钥串中寻找 Alice 自己的公钥。
  5. PGP 使用 Alice 的公钥对 Bob 的公钥上的 Alice 的数字签名进行验证。如果验证成功,则可以确认这的确就是 Bob 的公钥。
  6. PGP 使用合法的 Bob 的公钥对邮件上附带的 Bob 的数字签名进行验证。

2.场景2:通过自己完全信任的人的数字签名进行确认

Alice 有一个叫 Trent 的男朋友。在 Alice 的公钥串中,也包含带有 Alice 的数字签名的 Trent 的公钥。 Alice 非常信任 Trent ,她想:经过他签名的公钥一定是合法的
假设 Alice 收到了一封来自 Carrol 的邮件

8 本章小结

9 小测验