PHP empty函数测试对象使用魔术方法获取的属性

从PHP官方文档可以看到,empty用于测试一个变量是否为空,empty($var)等价于

1
!isset($var) || $var == false

empty方法的参数只能是一个变量,当传入一个对象的属性时,如果该属性是真实存在的,empty可以正常工作。
如果该属性是通过魔术方法获取的,empty的返回结果不是期望中的,而总是返回true。

PHP版本为5.6.24

1
2
3
4
# php --version
PHP 5.6.24 (cli) (built: Aug 1 2016 14:48:54)
Copyright (c) 1997-2016 The PHP Group
Zend Engine v2.6.0, Copyright (c) 1998-2016 Zend Technologies
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
class Test1 {
protected $attributes = [
'name' => 'Test1',
];
public function __get($attr)
{
return isset($this->attributes[$attr]) ? $this->attributes[$attr] : null;
}
}
class Test2 {
public $name = 'Test2';
}
$test1 = new Test1();
print $test1->name ;
//-- Test1
if (empty($test1->name)) {
print 'test1\'s name is empty';
} else {
print 'test1\' name is not empty';
}
//-- test1's name is empty
$test2 = new Test2();
print $test2->name ;
//-- Test2
if (empty($test2->name)) {
print 'test2\'s name is empty';
} else {
print 'test2\' name is not empty';
}
//-- test2's name is not empty

CURD model for Redis in Laravel style

Features

  • Supported operations: create, insert, find, destroy and so on
  • Fluent query builder
  • Use “multi” and “exec” for batch operation

Installation

This library could be found on Packagist for an easier management of projects dependencies using Composer.

Github repo: redmodel

Usage

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
use Limen\RedModel\Examples\HashModel;
// constructing parameters are passed transparently to Predis client's constructor
$hashModel = new HashModel([
'scheme' => 'tcp',
'host' => '127.0.0.1',
'port' => 6379,
]);
$maria = [
'name' => 'Maria',
'age' => '22',
'nation' => 'USA',
'state' => 'New York',
];
$cat = [
'name' => 'Catherine',
'age' => '23',
'nation' => 'UK',
'city' => 'London',
];
$tested = [];
// insert
$hashModel->insert(['id' => 1], $maria);
$hashModel->insert(['id' => 2], $cat);
// find by primary key
$user = $hashModel->find(1);
if ($user === $maria) {
$tested[] = 'Find OK';
}
// find by query
$users = $hashModel->where('id', 1)->get();
if ($users === [$maria]) {
$tested[] = 'Where then get OK';
}
$user = $hashModel->where('id', 1)->first();
if ($user === $maria) {
$tested[] = 'Where then first OK';
}
$users = $hashModel->whereIn('id', [1,2])->get();
if ($users === [$maria, $cat]) {
$tested[] = 'Where in then get OK';
}
// find batch by primary keys
$users = $hashModel->findBatch([1,2]);
if ($users === [$maria, $cat]) {
$tested[] = 'find batch OK';
}
// update by query
$hashModel->where('id', 1)->update([
'age' => '23',
]);
$user = $hashModel->find(1);
if ($user['age'] === '23') {
$tested[] = 'Update OK';
}
// remove item
$hashModel->destroy(1);
$user = $hashModel->find(1);
if (!$user) {
$tested[] = 'Destroy OK';
}
var_dump($tested);

Operation notices

create

Can use when a model’s key representation has only one dynamic field.

The default choice is “forced”, which would replace the same key if exists.

insert

The default choice is “forced”, which would replace the same key if exists.

Redis native methods

Redis native methods such as “set”, “hmset” can use when the query builder contains only one valid query key.

// string model
$model->where('id', 1)->set('maria');

// hash model
$model->where('id', 1)->update([
    'name' => 'Maria',
    'age' => '22',
]);
// equal to
$model->where('id', 1)->hmset([
    'name' => 'Maria',
    'age' => '22',
]);

Query builder

Taking the job to build query keys for model.

1
2
3
4
5
// model's key representation user:{id}:{name}
$queryBuilder->whereIn('id', [1,2])->where('name', 'maria');
// built keys
// user:1:maria
// user:2:maria

The built query keys which contain unbound fields would be ignored. For example

1
user:1:{name}

fileflake:适用于laravel框架的分布式文件存储服务

为Laravel定制的分布式文件存储服务,使用mongodb作为后端存储引擎。

特性

  • 支持的操作:上传,下载,删除
  • 分布式的文件存储节点
  • 存储节点负载均衡
  • 易于横向扩展(添加存储节点)
  • 文件流存储于mongodb
  • 文件流分块存储,块大小可配置
  • 拥有同样签名的文件只存储一个拷贝

上手

安装

This library could be found on Packagist for an easier management of projects dependencies using Composer.

Github仓库:fileflake

使用

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
use Limen\Fileflake\Config;
use Limen\Fileflake\Fileflake;
use Limen\Fileflake\Protocols\OutputFile;
class FileController
{
protected $config = [
Config::KEY_FILE_META_CONNECTION => 'mongodb_fileflake', // file meta connection
Config::KEY_FILE_META_COLLECTION => 'FileMeta', // file meta collection
Config::KEY_NODE_META_CONNECTION => 'mongodb_fileflake', // node meta connection
Config::KEY_NODE_META_COLLECTION => 'NodeMeta', // node meta collection
Config::KEY_FILE_CHUNK_SIZE => 4194304, // chunk size on byte
// if set to true, the load balance would consider file count and file volume of each storage node,
// or the load balance would pick one node randomly
Config::KEY_LOAD_BALANCE_STRICT => false,
Config::KEY_STORAGE_NODES => [
[
'id' => 1, // storage node id, should be unique and unmodifiable
'connection' => 'mongodb_fileflake', // storage node connection
'collection' => 'FileStore1', // storage node collection
],
[
'id' => 2,
'connection' => 'mongodb_fileflake',
'collection' => 'FileStore2',
],
],
Config::KEY_LOCALIZE_DIR => '/home/www/tmp/fileflake/local', // the temp local files stored in this directory
Config::KEY_LOCKER_FILES_DIR => '/home/www/tmp/fileflake/locker', // the locker files stored in this directory
];
public function upload()
{
$fileflake = new Fileflake($this->config);
// file id is a string of 32 characters
$fildId = $fileflake->put(
'/tmp/abc', // file local path
'tulips.jpg', // file name
'879394', // file size on byte
'jpg', // file extension
'mime/jpeg' // file mime
);
}
public function download()
{
$fileflake = new Fileflake($this->config);
/** @var OutputFile $file */
$file = $fileflake->get('5031a3057c8cff6fde3a4118187798bb');
}
public function remove()
{
$fileflake = new Fileflake($this->config);
$fileflake->remove('5031a3057c8cff6fde3a4118187798bb');
}
}

存储架构

文件元数据

每个文件都有一个元数据。

元数据的“引用”类似于Linux文件系统的软链接。

“引用计数”表示目前有多个文件指向当前文件(源文件)。

拥有相同签名的多个文件只存储一个拷贝,它们通过“引用”值与源文件产生关联。

当删除一个软链接文件时,删除文件元数据,软链接文件指向的源文件的引用计数减1。
当删除一个源文件时,源文件的引用计数减1。
当一个文件的引用计数为0时,删除该文件的元数据,并将该文件从后端存储中删除。

  • 文件id
  • 文件名
  • 文件签名
  • 引用计数
  • 引用(源文件id)
  • 存储节点id
  • 分块id
  • 扩展名
  • mime

存储节点

  • 分块id
  • 分块内容

节点元数据

存放存储节点元数据,用于负载均衡

  • 文件数量
  • 文件占用空间

github仓库

fileflake,欢迎star

抢红包背后的技术要点

最近做了一个抢红包的项目,这个项目涉及到了后端开发的多个技术点

  • 应用层悲观锁
  • 数据库锁机制
  • 数据库事务
  • 数据库索引

以上这些技术点都是为高并发场景服务的。

应用层悲观锁

为了使抢红包的请求能够依次处理,使用悲观锁将红包预先锁定。需要为该悲观锁设定一个最大生存时间,以确保发生不可预知的错误时,不会影响后续的用户抢红包。其余并发进入的请求将等待并争抢红包锁。一个请求处理完成,将红包解锁,下一个成功获取锁的请求将被处理。

处在等待状态的请求不断争抢红包锁。需设定一个最大重试次数,超时则请求处理过程结束。

实现悲观锁的方案有多种,这里采用了redis的原子操作setnx,setnx返回1代表争抢成功。

数据库锁机制

使用SELECT FOR UPDATE为数据加锁,保证同一时刻只能有一个请求更新数据。在应用层悲观锁的保护下,数据库锁争用、幻读的现象可以减少甚至避免,这样也降低了数据库本身的压力。

数据库事务

抢红包涉及到金钱,在发生异常时需回滚,保证数据一致性。

数据库索引

合理利用数据库索引可以避免不期望的结果。

红包的数据库采用了MySql,分两个表,主表和抢红包记录表。每个用户被限定最多只能抢一次红包,抢红包记录表中的唯一索引(红包id,用户id)可以避免发生意外。

经过应用层悲观锁、数据库锁的保护,发生这种意外的可能性已经微乎其微了。

一种红包分配算法及其实现

该算法适用于多人抢红包的场景,可动态调整红包分配金额的平均程度。红包余额需大于红包剩余份数,分配的金额为整数,如果需要分配成小数,将红包余额乘以100,分配结果除以100即可。

算法概述

红包余额R,红包剩余份数D,方差因子VF(>=0),分配金额G,每一份的最小金额为min

  1. D=1,则G=R
  2. D>1,按(R/D) + random(-(R/D)*VF, (R/D)*VF)将R循环分解为一个含有D个元素的列表,
    random方法取[a,b]范围的随机值。元素的值如果小于min,则取min,同时元素有一个动态的最大
    值max,保证后续元素的值均不小于min
  3. 从第2步得到的列表中随机选择一个元素作为G

容易看出

  • VF取0时,每个人分配到的金额都是R/D
  • VF的值越大,出现最小额度的概率越大,分配额度序列的方差越大(取为1较合理)
  • 第3步使得分配金额与抢红包的先后顺序无关

Python实现

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
import random
def avg(remain, dv):
return remain / dv
def randNoise(noise, expected = 0):
return random.randint(expected - noise, expected + noise)
def divideToList(remain, dv, vf, min):
divided = []
while dv > 0:
if dv == 1:
get = remain
else:
max = remain - (min * (dv-1))
av = avg(remain, dv)
get = av + randNoise(int(av * vf))
if get < min:
get = min
elif get > max:
get = max
divided.append(get)
dv = dv - 1
remain = remain - get
return divided
def assignToList(remain, dv, vf, min):
L = []
while dv > 0:
div = divideToList(remain, dv, vf, min)
g = random.choice(div)
L.append(g)
dv = dv-1
remain = remain - g
return L

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import sys
def testAssignment(L, dv):
if len(L) != dv:
print 'Bad #not average'
return False
for i in L:
if i <= 0:
print 'Bad #negative or zero'
return False
return True
remain = int(sys.argv[1])
dv = int(sys.argv[2])
vf = float(sys.argv[3])
min = int(sys.argv[4])
testTimes = int(sys.argv[5])
while testTimes > 0:
L = assignToList(remain, dv, vf, min)
if testAssignment(L, dv) == True:
print 'Good'
print L
testTimes = testTimes - 1

输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ python red_env.py 999 10 1.0 1 10
Good
[191, 114, 185, 122, 101, 23, 47, 8, 139, 69]
Good
[60, 41, 80, 139, 68, 30, 199, 268, 113, 1]
Good
[72, 149, 120, 143, 116, 12, 27, 154, 183, 23]
Good
[137, 180, 29, 120, 92, 26, 1, 249, 85, 80]
Good
[117, 118, 55, 69, 150, 151, 2, 34, 44, 259]
Good
[87, 7, 18, 88, 40, 9, 129, 334, 80, 207]
Good
[181, 125, 16, 69, 115, 9, 86, 179, 168, 51]
Good
[33, 55, 198, 183, 1, 209, 128, 83, 16, 93]
Good
[19, 55, 266, 234, 116, 60, 103, 50, 69, 27]
Good
[122, 85, 70, 10, 288, 127, 6, 98, 163, 30]

快速排序算法及其测试算法的原理与实现

快速排序简介

快速排序是一种分治的排序算法,是实践中最快的排序算法,理论上的时间复杂度为O(N*lgN),最差情况的时间复杂度为O(N^2),但稍加努力就可避免这种情况。

快速排序的步骤

  1. 如果列表中的元素为0或1个,则返回
  2. 选取标记值p
  3. 将列表分为两部分s1、s2,需满足条件:s1中的元素均小于或等于p,s2中的元素均大于等于p,得到s1+p+s2
  4. 对s1、s2重复2、3步骤,最终得到排序结果

从上面的步骤可以看出,快速排序需要递归运算。

Python实现

采用三值取中间值的方法选取标记值,从而避免最差情况。

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
def median3(L,a,b,c):
if L[a] >= L[b] and L[b] >= L[c]:
mid = L[b]
i = b
elif L[a] <= L[b] and L[b] <= L[c]:
mid = L[b]
i = b
elif L[b] >= L[a] and L[a] >= L[c]:
mid = L[a]
i = a
elif L[b] <= L[a] and L[a] <= L[c]:
mid = L[a]
i = a
else:
mid = L[c]
i = c
return [mid,i]
def swap(L, i, j):
tmp = L[i]
L[i] = L[j]
L[j] = tmp
def quickSort(L, low, high):
if low < high:
i = low
j = high
meta = median3(L, i, (i+j)/2, j)
pivot = meta[0]
pivotPos = meta[1]
# move pivot to the right end
swap(L, pivotPos, high)
while i < j:
# pivot on the right end, starting from left
while i < j and L[i] <= pivot:
i = i+1
while j > i and L[j] >= pivot:
j = j-1
swap(L, i, j)
# move pivot to right position
swap(L, i, high)
quickSort(L, low, i-1)
quickSort(L, i+1, high)

测试算法

排序结果正确,需满足条件

  1. 列表除元素顺序变化外,没有别的变化
  2. 列表中的元素是有序的

条件2容易实现,重点关注条件1

如何确保列表除元素顺序变化外,没有别的变化?

列表L1、L2满足以下三个条件即可

  1. L1、L2中的元素数量相同
  2. L1、L2的元素组成的集合相同(L1中的元素都在L2中,反之也成立)
  3. L1、L2中元素的和相同

例如,列表L1=[2,3,2,2,3],顺序打乱后得到L2=[2,2,3,3,2],此时L1、L2满足以上三个条件。如果对L2进行以下操作,均会使其中的一个或多个条件不成立。

  • 添加/删除元素
  • 修改元素值

测试算法实现

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
import sys
import random
import copy
# condition 2
def diff2List(l1,l2):
diff = [val for val in l1 if val not in l2]
for v in [val for val in l2 if val not in l1]:
diff.append(v)
return diff
def isListSorted(L):
i = 0
while i < len(L) - 2:
if L[i] <= L[i+1]:
i = i+1
else:
return [i, L[i], L[i+1]]
return True
# condition 3
def sumList(L):
sum = 0
for i in L:
sum = sum + i
return sum
def randomList(length, maximum):
l = []
i = 0
while i < length:
l.append(random.randint(0, maximum))
i = i+1
return l
#
# Test usage: python <script_path> <test_times> <min_list_length> <max_list_length> <max_list_element_value>
#
testTimes = int(sys.argv[1])
minLength = int(sys.argv[2])
maxLength = int(sys.argv[3])
maxValue = int(sys.argv[4])
for i in range(testTimes):
L = randomList(random.randint(minLength, maxLength), maxValue)
print 'Test round #' + str(i)
# original
print L
# deep copy for test
tmpList = copy.deepcopy(L)
quickSort(L, 0, len(L) - 1)
if len(L) != len(tmpList) or diff2List(L, tmpList) != [] or sumList(L) != sumList(tmpList):
print 'Bad #not coherent'
break
else:
sorted = isListSorted(L)
if sorted != True:
print 'Bad #not sorted'
print sorted
break
# after sort
print L

MongoDB还是MySQL?

MongoDB和MySql是目前最常用的两种数据库。面对一个应用场景,选择Mongodb还是MySql,哪一个更合适?

弄清两者的区别,就是对这个问题最好的回答。从schema、事务支持、关联查询三个方面进行说明。

schema

schema可以理解为约束。MySql的每个数据表都有一个schema,指明了每个字段的类型,这些类型就是对写入数据的约束。例如,一个字段为int型的字段,写入”helloworld”就会因类型不匹配而报错,导致写入失败;一个int unsigned类型的字段,写入-1同样会报错。

schema除了可以约束字段类型,还可以约束索引。例如建立一个唯一索引(userid,classid),假定数据库中已经有一条(userid=100,classid=1)的记录,如果此时再尝试写入一条userid=100,classid=1的数据,就会因为违反了唯一索引的限制而导致写入失败。

MongoDB作为一种schemaless的数据库,对写入的数据是没有类型限制的,这一点也是MongoDB与MySql之间最重要的一个区别。

如果一个应用的数据结构比较固定,需要约束字段类型和索引,则选用MySQL是合适的,反之则应该选用MongoDB。

事务支持

MySQL的InnoDB存储引擎支持事务,这使得MySQL适合那些对一致性要求较高的应用,在代码运行异常或出现业务异常时可以及时回滚。

截至目前最新的3.2版本,MongoDB还不支持事务。

如果一个应用场景需要更新多个表,在发生异常时需回滚数据,未发生异常时提交数据,选用MySQL是合适的。

关联查询

MySQL作为一种关系型数据库,天然支持联表查询。如果一个应用的数据结构需拆分成多个表,且表之间有关联关系,应选用MySQL。

Mongodb(<=3.2)不支持联表查询,这意味着一个查询只能针对一个表。

数据库设计的重要性与几个原则

随着工作经验的积累,我日益感觉到,对一名程序员来说,拥有良好的数据库设计能力是很重要的,甚至是最重要的。

程序员界有一句著名的话

Talk is cheap, show me the code

把这句话演变一下,就成了

Code is boring, show me the data structure

面对同样的数据结构,一百个程序员会写出一百种风格的代码。看别人写的代码,往往是很boring的。

数据结构为何如此重要

代码是围绕数据结构运行的。

客户端展现的动态数据,都是存储在数据库中,这对程序员来说一定是常识了。

为了便于阐述,我们拿简书的文章页面作为样板。

文章的作者、标题、正文、评论、喜欢等等,只要你打开任意两篇文章,两个页面不一样的地方,几乎都是因为在数据库中存储的内容不同。

良好的数据结构可以提升性能,使代码变得简单、清晰。数据结构清晰了,围绕着数据运行的代码自然就清晰了。

数据库设计需考虑的因素

提到数据库设计原则,首先会想到第一、第二、第三范式,这些理论能了解最好,本文不再赘述了。

从实践的角度面对一个具体的应用场景,设计数据库时应遵循哪些原则?

满足当前需求

数据结构的设计要能达到应用场景的要求,这是最基本的。举个例子,文章的正文存储在了数据表中的某个字段,该字段的长度被设定为10000字,在文章字数没有被限制在10000字以内的前提下,这显然不能满足应用场景的当前需求。需要考虑,什么样的字段类型才能存储大规模的文本数据?

分离主体与附属

文章页面中的元素,哪些是主体部分,哪些是附属部分?

一篇文章可以没有评论,评论的有无、多少不影响文章本身的完整性,评论可以被添加、删除。由此可见,文章的评论属于附属部分。阅读次数、喜欢该文章的用户与数量同样如此。

主体

  • 作者
  • 标题
  • 正文(字数)
  • 发布时间
  • 更新时间

附属

  • 阅读次数
  • 评论
  • 喜欢该文章的用户与数量

拆分的好处在于,首先数据结构更清晰了,其次可以提高读写性能。当文章有了新评论,只需更新存放评论的表。如果不拆分,需要更新的记录占用的磁盘空间很大,这对磁盘IO速度是个考验。

适当的冗余

或许你已经注意到了,文章的标题下面有这篇文章的字数。计算文章的字数,有两个时机:

  • 保存文章时
  • 读取文章时

后者的优势在于数据表中少了一个字段,而且这个字段不是必需的。哪个时机更好?个人觉得前者更好,理由如下

  • 计算长篇文章的字数是比较耗时的,应尽量减少计算次数
  • 总体来看,文章的保存次数远小于读取次数

如果能够提高应用的性能,适当的冗余是必要的。

页面的头部有文章作者的昵称,这适合作为冗余字段存储在文章主体数据中吗?用户可以随时更改自己的昵称,如果将昵称作为冗余字段,需要额外的工作以保持数据一致性,从这一点看,用户昵称不适合作为冗余字段。

选择作为冗余的字段应不需要额外的工作来保持数据一致性。

应对可能出现的新需求

如何存储喜欢文章的用户信息才能做智能推荐?一个好的数据结构应该能应对可能出现的新需求。

为了达到应用的要求,最简单的方式是将这些用户放在一条记录里,存储的字段可以是数组类型。这样设计,喜欢文章的用户信息与用户数量都能轻易获取,读写性能也很好。但对于“喜欢该文章的人还喜欢了”此类的智能推荐,这样的设计明显是难以应对的。将用户放在数组里支持“查询喜欢某文章的用户”,对“查询某用户喜欢的文章”的支持就很差或者根本做不到了,这是一种单向查询的数据结构。

应对大数据量

随着用户量不断增加,网站业务数据越来越多,文章数量也达到了百万级。这时如果只把文章存在一张数据表里,读写性能必然是会急剧下降的,这可能会导致用户体验变差,用户流失。老板不能容忍,DBA也不能容忍。

合理的解决方案之一是分为两张数据表,一张存储热门文章,另一张存储非热门文章。热门文章的占比很少,相应的加载速度就会好于非热门文章。

结尾

本文总结了设计数据库时需遵守的几个原则

  • 满足当前需求
  • 分离主体与附属
  • 适当的冗余
  • 应对可能出现的新需求
  • 应对大数据量

认识到数据结构的重要性,才能设计出好的数据结构。

代码重构之解耦合

最近在对业务代码进行重构,遇到了一些比较典型的“散发着难闻味道”的代码,可以用又臭又长来形容。

这部分的业务是发布动态,包括以下步骤:

  • 敏感词过滤
  • 话题提取
  • 动态数据入库
  • 敏感词记录
  • 话题及话题动态关联关系入库

之所以说重构前的代码“又臭又长”,首先最直观的一点,方法的行数超过了200行,其次上面四个步骤的逻辑全都写在了一起,没有按功能模块拆分,这也是方法过长的原因。

如果按功能划分,发布动态包含三个模块:

  • 敏感词模块
  • 动态模块
  • 话题模块

其中的敏感词模块话题模块都是为动态模块服务的。一个动态可以有敏感词,也可以没有敏感词,比如用户分享一个链接,这就不需要做敏感词过滤与记录了,所以敏感词模块与动态模块应该是“松耦合”的。同样,一个动态可以包含话题,也可能不包含,所以话题模块与动态模块也应该是松耦合的。

既然敏感词模块与话题模块都是为动态模块服务的,动态模块就有必要与这两个服务模块建立一个服务约定

  • 我需要什么样的服务
  • 我们之间如何建立耦合关系

我需要什么样的服务

在编程的世界里,约定可以抽象成接口

1
2
3
4
5
6
7
8
interface PublishContract
{
// Call this method before publishing
public function beforePublish();
// Call this method after publishing
public function afterPublish();
}

对于敏感词模块和话题模块,只需要实现该接口,动态模块就可以根据约定调用它们提供的服务。

1
2
3
4
5
6
7
8
9
10
11
12
class SensitiveWords implements PublishContract
{
public function beforePublish()
{
//
}
public function afterPublish()
{
//
}
}
1
2
3
4
5
6
7
8
9
10
11
12
class TopicObserver implements PublishContract
{
public function beforePublish()
{
//
}
public function afterPublish()
{
//
}
}

这样动态模块就知道,在动态入库前调用服务模块的beforePublish服务,入库后调用服务模块的afterPublish服务。

我们如何建立耦合

耦合关系的建立应该由动态发布的调用方决定,动态模块需提供相应的接口,

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
class Feeds
{
// PublishContract[]
protected $serviceModules = [];
public function addServiceModule(PublishContract $service)
{
$this->serviceModules[get_class($service)] = $service;
}
public function removeServiceModule($serviceClassName)
{
unset($this->serviceModules[$serviceClassName]);
}
public function publish()
{
foreach ($this->serviceModules as $service) {
$service->beforePublish();
}
// do something about publishment
foreach ($this->serviceModules as $service) {
$service->afterPublish();
}
}
}

这样,动态发布的调用方可以通过addServiceModule/removeServiceModule动态地添加/删除服务模块。

UML图

[from processon.com](https://www.processon.com/i/5429053c0cf2e6eabf125bb8 "")

重构带来的好处

松耦合

服务模块与核心模块是独立的,通过接口建立松耦合的关系;可动态添加/删除服务模块。

易于维护

服务模块的修改不会直接影响核心模块。

易于扩展

如果需要添加其它服务模块,只需实现服务约定的接口,并将新的服务模块动态添加到核心模块即可。

为数据库添加外部缓存带来的性能提升分析

数据库

指持久化数据库,如

  • mysql
  • mongodb

缓存

指内存型的数据存储,如

  • redis
  • memcached

一个良好的缓存策略需兼顾

  • 命中率
  • 缓存数据与数据库数据的一致性

对命中率的兼顾

多大的命中率是好的?

用数学知识来分析这个问题,先设定几个关键的参数

  • T(c):读一次缓存所需时间
  • T(d):读一次数据库所需时间
  • G:缓存命中率

一次数据读取所需的时间的期望值为:

1
T(c)*G + (T(c)+T(d))*(1-G)

增加数据库缓存的目标为提高数据读取速度,可以归结为一个表达式

1
T(c)*G + (1-G)*(T(c)+T(d)) < T(d)

等同于

1
T(c) - T(d)*G < 0

读取速度提升的比值为

1
(T(d)*G - T(c))/T(d)

假如,T(d)=50ms,T(c)=10ms,G=0.6,读取速度提升比值为

1
(50*0.6 - 10)/50 = 0.4

化为百分比也就是40%。

假如,T(d)=50ms,T(c)=10ms,G的值必须大于

1
G = T(c)/T(d) = 0.2

才能期望读取速度得到提升。

可以看出

加入缓存不一定能够提升读取性能,这取决于缓存读取速度、数据库读取速度以及缓存的命中率。