通过责任链模式设计多级缓存

背景:最近业务上面有一个多级缓存的需求,也很简单,内存 -> 文件 -> MySQL。开始的时候,我只做了 文件 -> MySQL 的缓存,但是,后面我又加了一个内存作为缓存,并且,我在测试的时候,发现我忘了在读取到下一级的文件缓存数据之后,更新到上一级的内存缓存。于是我就发现了这很容易造成缓存没更新的问题,所以调研了一下,发现责任链设计模式可以很好的解决这个问题。

我们可以先来看一看不用责任链模式,我的代码是如何写的。首先是只有 文件 -> MySQL 的缓存:

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 function get(string $configKey): array
{
$filePath = $configKey . DIRECTORY_SEPARATOR . 'cache.json';

// 先查看文件里面有没有缓存配置信息
$result = $this->readFromCachedFile($filePath);
if (! empty($result)) {
$this->logger->debug('read config from cached file', ['path' => $filePath]);

return $result;
}

/** @var Config */
$config = Config::query()->where('id', $configKey)->first();

$this->logger->debug('query config from mysql');

$payload = json_decode($config, true);

$result['gslb'] = $payload['gslb'];
$result['sdkconfig'] = $payload['sdkconfig'][$role] ?? [];

$this->writeToCachedFile($filePath, json_encode($result));
$this->logger->debug('write config to cached file', ['filePath' => $filePath]);

return $result;
}

可以看到,代码可读性还是不错的。先查找文件,然后再查找数据库,然后更新文件。一路下来,没有任何难题。

但是,当我变成了 内存 -> 文件 -> MySQL 缓存之后,问题开始凸显出来了。我的第一版代码是这样的:s

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
public function get(string $configKey): array
{
// 先查看内存里面有没有缓存配置信息
$result = $this->readFromCachedMemory($configKey);
if (! empty($result)) {
$this->logger->debug('read config from cached memory', ['path' => $configKey]);
return $result;
}

$filePath = $configKey . DIRECTORY_SEPARATOR . 'cache.json';

// 先查看文件里面有没有缓存配置信息
$result = $this->readFromCachedFile($filePath);
if (! empty($result)) {
$this->logger->debug('read config from cached file', ['path' => $filePath]);

return $result;
}

/** @var Config */
$config = Config::query()->where('id', $configKey)->first();

$this->logger->debug('query config from mysql');

$payload = json_decode($config, true);

$result['gslb'] = $payload['gslb'];
$result['sdkconfig'] = $payload['sdkconfig'][$role] ?? [];

$this->writeToCachedMemory($configKey, json_encode($result));
$this->logger->debug('write config to cached memory', ['path' => $configKey]);
$this->writeToCachedFile($filePath, json_encode($result));
$this->logger->debug('write config to cached file', ['filePath' => $filePath]);

return $result;
}

咋眼一看,可能还真看不出啥问题。但是编写完足够的单元测试之后,问题就凸显出来了。我发现,这段代码有问题:

1
2
3
4
5
6
7
// 先查看文件里面有没有缓存配置信息
$result = $this->readFromCachedFile($filePath);
if (! empty($result)) {
$this->logger->debug('read config from cached file', ['path' => $filePath]);

return $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
// 查看一级缓存
$result = $this->readFromFirstCache($configKey);
if (! empty($result)) {
return $result;
}

// 查看二级缓存
$result = $this->readFromSecondCache($configKey);
if (! empty($result)) {
$this->writeToFirstCache($configKey, $result);
return $result;
}

// 查看三级缓存
$result = $this->readFromThreeCache($configKey);
if (! empty($result)) {
$this->writeToSecondCache($configKey);
$this->writeToFirstCache($configKey, $result);
return $result;
}

// 查看数据库
$result = $this->readFromMySQL($configKey);
if (! empty($result)) {
$this->writeToThreeCache($configKey);
$this->writeToSecondCache($configKey);
$this->writeToFirstCache($configKey, $result);
return $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
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
<?php

abstract class CachedChain
{
public const MEMORY = 1;

public const FILE = 2;

public const MYSQL = 3;

/**
* @var int
*/
protected $level;

/**
* @var CachedChain
*/
protected $nextCache;

/**
* @var string
*/
public $data = '';

abstract protected function read(array $param): string;

public function setNextCache(CachedChain $nextCache)
{
$this->nextCache = $nextCache;
}

public function readContent(int $level, array $param): string
{
$content = "";

if ($this->level >= $level) {
$content = $this->read($param);
}

if (!empty($content)) {
return $content;
}

$content = $this->nextCache->readContent($level, $param);
$this->data = $content;
return $content;
}
}

class CachedMemory extends CachedChain
{
protected $level = self::MEMORY;

protected function read(array $param): string
{
return $this->data;
}
}

class CachedFile extends CachedChain
{
protected $level = self::FILE;

protected function read(array $param): string
{
return $this->data;
}
}

class CachedDB extends CachedChain
{
protected $level = self::MYSQL;

protected function read(array $param): string
{
return $this->data;
}
}

$memory = new CachedMemory;
$file = new CachedFile;
$db = new CachedDB;
$db->data = 'hello world';

$memory->setNextCache($file);
$file->setNextCache($db);

$data = $memory->readContent(CachedChain::MEMORY, []);
var_dump($data);
$data = $memory->readContent(CachedChain::MEMORY, []);
var_dump($data);

这个代码运行结果:

1
2
string(11) "hello world"
string(11) "hello world"

感兴趣的小伙伴可以调试一下。然后稍加修改,就可以做成一个通用的组件了。