项目实战

高性能缓存

从0开始迭代,一步步设计并实现

缓存是在实际生产中非常常用的工具,使用缓存可以避免重复计算,提高吞吐量

最初级的缓存可以用一个Map来实现,但一个功能完备,性能强劲的缓存,需要考虑的点就非常多了,从最简单的HashMap入手,一点点提高项目缓存的性能

最简单缓存

HashMap

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
/**
* 描述:最简单的缓存形式:HashMap
*/
public class ImoocCache1 {

private final HashMap<String,Integer> cache = new HashMap<>();

public Integer computer(String userId) throws InterruptedException {
Integer result = cache.get(userId);
//先检查HashMap里面有没有保存过之前的计算结果
if (result == null) {
//如果缓存中找不到,那么需要现在计算一下结果,并且保存到HashMap中
result = doCompute(userId);
cache.put(userId, result);
}
return result;
}

private Integer doCompute(String userId) throws InterruptedException {
TimeUnit.SECONDS.sleep(5);
return new Integer(userId);
}

public static void main(String[] args) throws InterruptedException {
ImoocCache1 imoocCache1 = new ImoocCache1();
System.out.println("开始计算了");
Integer result = imoocCache1.computer("13");
System.out.println("第一次计算结果:"+result);
result = imoocCache1.computer("13");
System.out.println("第二次计算结果:"+result);

}
}

这样的写法有一些问题

在多线程的情况下是并发不安全的

可以在此情况下进行升级

使用synchronized实现

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
/**
* 描述:最简单的缓存形式:HashMap
*/
public class ImoocCache1 {

private final HashMap<String,Integer> cache = new HashMap<>();

public synchronized Integer computer(String userId) throws InterruptedException {
Integer result = cache.get(userId);
//先检查HashMap里面有没有保存过之前的计算结果
if (result == null) {
//如果缓存中找不到,那么需要现在计算一下结果,并且保存到HashMap中
result = doCompute(userId);
cache.put(userId, result);
}
return result;
}

private Integer doCompute(String userId) throws InterruptedException {
TimeUnit.SECONDS.sleep(5);
return new Integer(userId);
}

public static void main(String[] args) throws InterruptedException {
ImoocCache1 imoocCache1 = new ImoocCache1();
System.out.println("开始计算了");
Integer result = imoocCache1.computer("13");
System.out.println("第一次计算结果:"+result);
result = imoocCache1.computer("13");
System.out.println("第二次计算结果:"+result);

}
}

使用synchronized有如下缺点

性能差

代码复用性差

给HashMap加final关键字

属性被声明为final后,该变量则只能被赋值一次,一旦被赋值了,fianl变量就不能被改变

解决代码复用性

使用装饰者模式

假设ExpensiveFunction类是耗时计算的实现类,实现了Computable接口,但是其本身不具有缓存的功能,也不考虑缓存的事情

1
2
3
4
5
6
7
/**
* 描述:有一个计算函数computer,用来代表耗时计算,每个计算器都要实现这个接口,这样就可以无侵入实现缓存功能
*/
public interface Computable <A,V>{

V compute(A arg) throws Exception;
}
1
2
3
4
5
6
7
8
9
10
11
/**
* 描述:耗时计算的实现类,实现了Computable接口,但是本身不具备缓存能力,不需要考虑缓存的事情
*/
public class ExpensiveFunction implements Computable<String, Integer>{

@Override
public Integer compute(String arg) throws Exception {
Thread.sleep(5000);
return Integer.valueOf(arg);
}
}
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
/**
* 描述: 用装饰者模式,给计算器自动添加缓存功能
*/
public class ImoocCache2<A,V> implements Computable<A,V> {

private final Map<A, V> cache = new HashMap();

private final Computable<A,V> c;

public ImoocCache2(Computable<A, V> c) {
this.c = c;
}

@Override
public synchronized V compute(A arg) throws Exception {
System.out.println("进入缓存机制");
V result = cache.get(arg);
if (result == null) {
result = c.compute(arg);
cache.put(arg, result);
}
return result;
}

public static void main(String[] args) throws Exception {
ImoocCache2<String, Integer> expensiveComputer = new ImoocCache2<>(
new ExpensiveFunction());
Integer result = expensiveComputer.compute("666");
System.out.println("第一次计算结果:"+result);
result = expensiveComputer.compute("13");
System.out.println("第二次计算结果:"+result);
}
}

不足

当多个线程同时想计算的时候,需要慢慢等待,严重的时候甚至比不用缓存性能更差

性能待优化

引出锁性能优化经验,缩小锁的粒度

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
/**
* 描述: 缩小了synchronized的粒度,提高性能,但是依然并发不安全
*/
public class ImoocCache4<A, V> implements Computable<A, V> {

private final Map<A, V> cache = new HashMap();

private final Computable<A, V> c;

public ImoocCache4(Computable<A, V> c) {
this.c = c;
}

@Override
public V compute(A arg) throws Exception {
System.out.println("进入缓存机制");
V result = cache.get(arg);
if (result == null) {
result = c.compute(arg);
synchronized (this) {
cache.put(arg, result);
}
}
return result;
}

public static void main(String[] args) throws Exception {
ImoocCache4<String, Integer> expensiveComputer = new ImoocCache4<>(
new ExpensiveFunction());
Integer result = expensiveComputer.compute("666");
System.out.println("第一次计算结果:" + result);
result = expensiveComputer.compute("666");
System.out.println("第二次计算结果:" + result);
}
}

虽然提高并发效率,但是并不意味着线程安全,还需要考虑同时读写等情况

使用ConcurrentHashMap

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
public class ImoocCache5<A, V> implements Computable<A, V> {

private final Map<A, V> cache = new ConcurrentHashMap<>();

private final Computable<A, V> c;

public ImoocCache5(Computable<A, V> c) {
this.c = c;
}

@Override
public V compute(A arg) throws Exception {
System.out.println("进入缓存机制");
V result = cache.get(arg);
if (result == null) {
result = c.compute(arg);
cache.put(arg, result);
}
return result;
}

public static void main(String[] args) throws Exception {
ImoocCache5<String, Integer> expensiveComputer = new ImoocCache5<>(
new ExpensiveFunction());
Integer result = expensiveComputer.compute("666");
System.out.println("第一次计算结果:" + result);
result = expensiveComputer.compute("666");
System.out.println("第二次计算结果:" + result);
}
}

此时也存在缺点

在计算完成前,另一个要求计算相同值的请求到来,会导致计算两遍,这和缓存想避免多次计算的初衷恰恰相反

如图

image-20200227104708583

避免重复计算

Future和Callable的使用

现在不同的线程进来之后,确实可以同时计算,但是如果两个线程相差无几的进来请求同一个数据,就会出现重复计算,若是更多的线程请求同样的内容,却都需要重新计算,则会造成更大的浪费

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
/**
* 描述: 利用Future,避免重复计算
*/
public class ImoocCache7<A, V> implements Computable<A, V> {

private final Map<A, Future<V>> cache = new ConcurrentHashMap<>();

private final Computable<A, V> c;

public ImoocCache7(Computable<A, V> c) {
this.c = c;
}

@Override
public V compute(A arg) throws Exception {
Future<V> f = cache.get(arg);
if (f == null) {
Callable<V> callable = new Callable<V>() {
@Override
public V call() throws Exception {
return c.compute(arg);
}
};
FutureTask<V> ft = new FutureTask<>(callable);
f = ft;
cache.put(arg, ft);
System.out.println("从FutureTask调用了计算函数");
ft.run();
}
return f.get();
}

public static void main(String[] args) throws Exception {
ImoocCache7<String, Integer> expensiveComputer = new ImoocCache7<>(
new ExpensiveFunction());
new Thread(new Runnable() {
@Override
public void run() {
try {
Integer result = expensiveComputer.compute("666");
System.out.println("第一次的计算结果:" + result);
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
Integer result = expensiveComputer.compute("666");
System.out.println("第三次的计算结果:" + result);
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
Integer result = expensiveComputer.compute("667");
System.out.println("第二次的计算结果:" + result);
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();


}
}

以上方法依然存在重复计算的可能

如果有两个同时计算666的线程,同时调用cache.get()方法,那么返回的结果都为null,后面还是会创建两个任务去计算相同的值

image-20200227110045352

计算抛出异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 描述: 耗时计算的实现类,有概率计算失败
*/
public class MayFail implements Computable<String, Integer>{

@Override
public Integer compute(String arg) throws Exception {
double random = Math.random();
if (random > 0.5) {
throw new IOException("读取文件出错");
}
Thread.sleep(3000);
return Integer.valueOf(arg);
}
}
文章目录
  1. 1. 项目实战
    1. 1.1. 最简单缓存
    2. 1.2. 使用synchronized实现
      1. 1.2.1. 给HashMap加final关键字
    3. 1.3. 解决代码复用性
    4. 1.4. 性能待优化
    5. 1.5. 使用ConcurrentHashMap
    6. 1.6. 避免重复计算
    7. 1.7. 计算抛出异常
|