DB主从一致性架构优化4种方法

需求缘起

大部分互联网的业务都是“读多写少”的场景,数据库层面,读性能往往成为瓶颈。如下图:业界通常采用“一主多从,读写分离,冗余多个读库”的数据库架构来提升数据库的读性能。

这种架构的一个潜在缺点是,业务方有可能读取到并不是最新的旧数据:

(1)系统先对DB-master进行了一个写操作,写主库

(2)很短的时间内并发进行了一个读操作,读从库,此时主从同步没有完成,故读取到了一个旧数据

(3)主从同步完成

有没有办法解决或者缓解这类“由于主从延时导致读取到旧数据”的问题呢,这是本文要集中讨论的问题。

方案一(半同步复制)

不一致是因为写完成后,主从同步有一个时间差,假设是500ms,这个时间差有读请求落到从库上产生的。有没有办法做到,等主从同步完成之后,主库上的写请求再返回呢?答案是肯定的,就是大家常说的“半同步复制
”semi-sync:

(1)系统先对DB-master进行了一个写操作,写主库

(2)等主从同步完成,写主库的请求才返回

(3)读从库,读到最新的数据(如果读请求先完成,写请求后完成,读取到的是“当时”最新的数据)

方案优点:利用数据库原生功能,比较简单

方案缺点:主库的写请求时延会增长,吞吐量会降低

方案二(强制读主库)

如果不使用“增加从库”的方式来增加提升系统的读性能,完全可以读写都落到主库,这样就不会出现不一致了:

方案优点:“一致性”上不需要进行系统改造

方案缺点:只能通过cache来提升系统的读性能,这里要进行系统改造

方案三(数据库中间件)

如果有了数据库中间件,所有的数据库请求都走中间件,这个主从不一致的问题可以这么解决:

(1)所有的读写都走数据库中间件,通常情况下,写请求路由到主库,读请求路由到从库

(2)记录所有路由到写库的key,在经验主从同步时间窗口内(假设是500ms),如果有读请求访问中间件,此时有可能从库还是旧数据,就把这个key上的读请求路由到主库

(3)经验主从同步时间过完后,对应key的读请求继续路由到从库

方案优点:能保证绝对一致

方案缺点:数据库中间件的成本比较高

方案四(缓存记录写key法)

既然数据库中间件的成本比较高,有没有更低成本的方案来记录某一个库的某一个key上发生了写请求呢?很容易想到使用缓存,当写请求发生的时候:

(1)将某个库上的某个key要发生写操作,记录在cache里,并设置“经验主从同步时间”的cache超时时间,例如500ms

(2)修改数据库

而读请求发生的时候:

(1)先到cache里查看,对应库的对应key有没有相关数据

(2)如果cache hit,有相关数据,说明这个key上刚发生过写操作,此时需要将请求路由到主库读最新的数据

(3)如果cache miss,说明这个key上近期没有发生过写操作,此时将请求路由到从库,继续读写分离

方案优点:相对数据库中间件,成本较低

方案缺点:为了保证“一致性”,引入了一个cache组件,并且读写数据库时都多了一步cache操作

总结

为了解决主从数据库读取旧数据的问题,常用的方案有四种:

(1)半同步复制

(2)强制读主

(3)数据库中间件

(4)缓存记录写key

前3个方案在今年数据库大会(DTCC2016)上share过,相关的材料在网上能下载到。第4个方案是大会现场有其他同学share的一个好方法,感谢这位同学。

MySQL 常用30种SQL查询语句优化方法

1、应尽量避免在 where 子句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫描。

2、对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引。

3、应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描。如:

select id from t where num is null

可以在num上设置默认值0,确保表中num列没有null值,然后这样查询:

select id from t where num=0

4、尽量避免在 where 子句中使用 or 来连接条件,否则将导致引擎放弃使用索引而进行全表扫描,如:

select id from t where num=10 or num=20

可以这样查询:

select id from t where num=10

union all

select id from t where num=20

5、下面的查询也将导致全表扫描:(不能前置百分号)

select id from t where name like ‘%c%’

下面走索引

select id from t where name like ‘c%’

若要提高效率,可以考虑全文检索。

6、in 和 not in 也要慎用,否则会导致全表扫描,如:

select id from t where num in(1,2,3)

对于连续的数值,能用 between 就不要用 in 了:

select id from t where num between 1 and 3

7、如果在 where
子句中使用参数,也会导致全表扫描。因为SQL只有在运行时才会解析局部变量,但优化程序不能将访问计划的选择推迟到运行时;它必须在编译时进行选择。然
而,如果在编译时建立访问计划,变量的值还是未知的,因而无法作为索引选择的输入项。如下面语句将进行全表扫描:

select id from t where num=@num

可以改为强制查询使用索引:

select id from t with(index(索引名)) where num=@num

8、应尽量避免在 where 子句中对字段进行表达式操作,这将导致引擎放弃使用索引而进行全表扫描。如:

select id from t where num/2=100

应改为:

select id from t where num=100*2

9、应尽量避免在where子句中对字段进行函数操作,这将导致引擎放弃使用索引而进行全表扫描。如:

select id from t where substring(name,1,3)=’abc’ –name以abc开头的id

select id from t where datediff(day,createdate,’2005-11-30′)=0
–’2005-11-30′生成的id

应改为:

select id from t where name like ‘abc%’

select id from t where createdate>=’2005-11-30′ and createdate<’2005-12-1′

10、不要在 where 子句中的“=”左边进行函数、算术运算或其他表达式运算,否则系统将可能无法正确使用索引。

11、在使用索引字段作为条件时,如果该索引是复合索引,那么必须使用到该索引中的第一个字段作为条件时才能保证系统使用该索引,否则该索引将不会被使
用,并且应尽可能的让字段顺序与索引顺序相一致。

12、不要写一些没有意义的查询,如需要生成一个空表结构:

select col1,col2 into #t from t where 1=0

这类代码不会返回任何结果集,但是会消耗系统资源的,应改成这样:

create table #t(…)

13、很多时候用 exists 代替 in 是一个好的选择:

select num from a where num in(select num from b)

用下面的语句替换:

select num from a where exists(select 1 from b where num=a.num)

14、并不是所有索引对查询都有效,SQL是根据表中数据来进行查询优化的,当索引列有大量数据重复时,SQL查询可能不会去利用索引,如一表中有字段
sex,male、female几乎各一半,那么即使在sex上建了索引也对查询效率起不了作用。

15、索引并不是越多越好,索引固然可以提高相应的 select 的效率,但同时也降低了 insert 及 update 的效率,因为 insert 或
update
时有可能会重建索引,所以怎样建索引需要慎重考虑,视具体情况而定。一个表的索引数较好不要超过6个,若太多则应考虑一些不常使用到的列上建的索引是否有 必要。

16.应尽可能的避免更新 clustered 索引数据列,因为 clustered
索引数据列的顺序就是表记录的物理存储顺序,一旦该列值改变将导致整个表记录的顺序的调整,会耗费相当大的资源。若应用系统需要频繁更新 clustered
索引数据列,那么需要考虑是否应将该索引建为 clustered 索引。

17、尽量使用数字型字段,若只含数值信息的字段尽量不要设计为字符型,这会降低查询和连接的性能,并会增加存储开销。这是因为引擎在处理查询和连接时会
逐个比较字符串中每一个字符,而对于数字型而言只需要比较一次就够了。

18、尽可能的使用 varchar/nvarchar 代替 char/nchar
,因为首先变长字段存储空间小,可以节省存储空间,其次对于查询来说,在一个相对较小的字段内搜索效率显然要高些。

19、任何地方都不要使用 select * from t ,用具体的字段列表代替“*”,不要返回用不到的任何字段。

20、尽量使用表变量来代替临时表。如果表变量包含大量数据,请注意索引非常有限(只有主键索引)。

21、避免频繁创建和删除临时表,以减少系统表资源的消耗。

22、临时表并不是不可使用,适当地使用它们可以使某些例程更有效,例如,当需要重复引用大型表或常用表中的某个数据集时。但是,对于一次性事件,较好使 用导出表。

23、在新建临时表时,如果一次性插入数据量很大,那么可以使用 select into 代替 create table,避免造成大量 log
,以提高速度;如果数据量不大,为了缓和系统表的资源,应先create table,然后insert。

24、如果使用到了临时表,在存储过程的最后务必将所有的临时表显式删除,先 truncate table ,然后 drop table
,这样可以避免系统表的较长时间锁定。

25、尽量避免使用游标,因为游标的效率较差,如果游标操作的数据超过1万行,那么就应该考虑改写。

26、使用基于游标的方法或临时表方法之前,应先寻找基于集的解决方案来解决问题,基于集的方法通常更有效。

27、与临时表一样,游标并不是不可使用。对小型数据集使用 FAST_FORWARD
游标通常要优于其他逐行处理方法,尤其是在必须引用几个表才能获得所需的数据时。在结果集中包括“合计”的例程通常要比使用游标执行的速度快。如果开发时
间允许,基于游标的方法和基于集的方法都可以尝试一下,看哪一种方法的效果更好。

28、在所有的存储过程和触发器的开始处设置 SET NOCOUNT ON ,在结束时设置 SET NOCOUNT OFF
。无需在执行存储过程和触发器的每个语句后向客户端发送 DONEINPROC 消息。

29、尽量避免向客户端返回大数据量,若数据量过大,应该考虑相应需求是否合理。

30、尽量避免大事务操作,提高系统并发能力。

如何在开发过程中搭建简单的假数据服务器

在开发一块新功能的过程中,我们通常会涉及到与后端接口联调的问题。新的界面、新的后端接口,这时候在开发的时候往往前端就很尴尬,后端不依赖前端,但是前端十分依赖后端。

大部分应用都会有或多或少地依赖后端数据,有的界面只需要简单搞个假数据传入即可,但是我们还需要应对很多复杂情况,比如:

  • 处理无数据、返回错误、各类非正常的状态;

  • 处理分页数据的情况;

  • 模拟一个请求中、请求失败、请求结果返回的情景;

这时候如果能够写一个简单的服务器,你请求真实的接口url,只需要给手机设置一个代理,就返回你设置的假数据。这样你就可以完全抛开其他依赖的顾虑,可以像正常情况一样开发、校验结果,而不是依赖写死在代码里的各种假逻辑(这样做也会为后面的开发带来一些隐患)。

实际上服务器本身的逻辑是非常简单的,但是要真正得搭建成你所需要的环境,需要一些复杂功夫。本文以[nodejs](http://blog.desmondyao.com
/fake-server/node)为例,具体描述一下如何在本机上搭建测试服务器。

使用node搭建起简单的服务器脚本

1. 安装

Unix 用户可以使用命令行安装nodejs。

Windows 用户可以直接下载安装包

安装成功后如果在命令行下输入node -v能够打印出版本信息,说明安装成功了。

2. 简单服务器

Node 服务器可以运行在“IP:PORT”上,我们可以通过如下代码来在127.0.0.1:2333端口上搭建起一个简单的服务器:

server.js

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


const http = require('http');

http.createServer( (req, res) => {

// 返回码200,返回头中标识内容为json

res.writeHead(200, {'Content-Type': 'text/json'});

let resp = {

code : 0,

time : 1469981217,

data : {

name : 'desmond',

gender : 'male'

}

};

res.end(JSON.stringify(resp)); // 将resp转换为JSON字符串返回。

}).listen(2333, '127.0.0.1', )

之后输入node server.js,然后我们到浏览器中访问localhost:2333就可以看见:

具体的信息可以参考Nodejs-http.

3. IDE配置

从IDE环境切换到脚本环境来写代码总会不顺手,推荐一个我认为比较容易上手的配置吧:

Nginx反向代理

nginx
搭建服务器的强大处之一就是它的代理能力,配置也十分简洁。

如果要模拟最终的请求的话,我们应该是原封不动的保留请求url: ‘www.desmond.com/api/something’。首先我们需要
在手机上设置代理,将ip配置到自己电脑上,端口配为80
。但是你的电脑识别到这个请求后,怎么让它导流到你的node服务器,这是一个问题。如果你代码写死的ip+端口来访问,未免有点太low了,我们既然折腾了这么多,那可以继续往下走一步:反向代理

反向代理,简而言之,就是一个分发请求的代理。前向代理
是直接发给目标服务器,但是它会做一些额外的处理工作。反向代理不一样,它自己相当于是一个服务器,请求到它手里,它根据请求去不同服务器上拉取数据。

1. 安装Nginx

Linux用户可以使用命令行:

1
2
3
apt-get update

apt-get install nginx

OSX用户可以用Homebrew:

1
brew install nginx

Windows用户可以下载安装包

2. 配置代理

我们希望针对’www.desmond.com/api/something’下的url交由node服务器(2333端口)处理,其他情况下继续发送,可以在nginx.conf里面这么配置(可以在命令行下输入nginx
-t找到配置文件位置):

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


server {

listen 本机ip:80;

server_name www.desmond.com;

#charset koi8-r;

access_log /Users/desmond/Nginx/api.access.log;

error_log /Users/desmond/Nginx/api.error.log;

location / { //默认情况原路继续

resolver 8.8.8.8;

proxy_pass http://$http_host$request_uri;

proxy_set_header Host $http_host;

proxy_connect_timeout 5;

}

location /api { //检测到api路径下的,转发到端口2333

proxy_pass http://localhost:2333;

}

#error_page 404 /404.html;

# redirect server error pages to the static page /50x.html

#

error_page 500 502 503 504 /50x.html;

location = /50x.html {

root html;

}

}

这里如果希望原路继续的那些url域名解析配合你的host(此处使用8.8.8.8来做DNS解析),你可以参考[StackOverflow的一个提问](http://stackoverflow.com/questions/8305015
/when-using-proxy-pass-can-etc-hosts-be-used-to-resolve-domain-names-instead-
of)。

这样一来,你所有手机上访问的www.desmond.com/api/something就导到你的nodejs服务器上啦,尽情配置假数据来测试吧~~

4. 配合Charles使用

如果使用Charles的话,手机上一般配的代理是”ip:8888”,那么此时需要做一件事:本地的host设置本机ip
www.desmond.com,这样才能保证 www.desmond.com域名下的请求被导流到nginx服务器,从而导流到自己的nodejs服务器上。

最终搭建

直接使用node,还是有一些繁琐的。既然我们的目的是“简单”,那么可以考虑一下使用express
。它封装了很多API,然呢使用起来非常方便。其中一项就是路由(Route)。它意思简单来说就是: www.desmond.com/api/a 由 a
的逻辑处理,www.desmond.com/api/b 由 b 的逻辑处理。

我相信一个新模块的服务器接口肯定不止一个,假如我们现在接口文档上写着:

1. 提交个人信息:www.desmond.com/api/personal

方法:POST

返回示例:

1
2
3
4
5
6
7
{

code : 0,

time : 1469981217

}

2. 获取未来n天天气信息:www.desmond.com/api/weather

方法:GET

参数:day 未来天数

返回示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

{

code : 0,

time : 1469981217,

data : {

items : [

date : '2016-08-02',

state : 'sunny'

],

//...

}

}

那么你可以使用来做一个简单的ROUTE+请求处理:

server.js

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
const express = require('express');

const url = require('url');

let app = express();

app.post('/api/personal', function (req, res) { //针对/api/personal 的 post请求返回

    res.writeHead(200, {'Content-Type': 'text/json;charset=utf-8'});

    res.end(JSON.stringify({

code : 0,

time : 1469981217

}));

});

app.get('/api/weather', function (req, res) { //针对/api/weather 的 get请求返回

    res.writeHead(200, {'Content-Type': 'text/json;charset=utf-8'});

let count = url.parse(req.url, true).query.day; //解析传入的day参数

if(count) { //若有,根据day参数生成item

let tmpList = [];

let tmpDate = newDate();

for(let i = 0; i < count; i++ ) {

tmpDate.setDate(tmpDate.getDate() + 1);

tmpList.push({

date : `${tmpDate.getFullYear()}-${tmpDate.getMonth() +
1}-${tmpDate.getDate()}`,

state : `sunny - ${i}`

});

}

res.end(JSON.stringify({

code : 0,

time : 1469981217,

items : tmpList

}));

} else { //若无,则返回错误信息

res.end(JSON.stringify({

code : -1,

msg: 'you must send param \"day\"',

time : 1469981217

}));

}

});

app.listen(2333, function(req, res) {

    console.log(`You have run node host.`);

});

注意:不要忘记安装express,(npm install express –save)即可。

编辑结束后运行一下node server.js,你可以看到输出:

You have run node host.

这时我们可以尝试请求一下 localhost:2333,可以看到返回:

大功告成~~

如果希望node server一直在后台跑(使用node
server.js时shell会卡在当前运行中),可以使用ForeverJS

更简单的办法

如果不想折腾太多,可以直接写一个json静态文件去返回:

data.json

1
2
3
4
5
6
7
{

"code" : 0,

"time" : 1469981217

}

*注意:手写json的话,里面的 key 必须以字符串形式(双冒号包围)存在。

server.js

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
const fs = require('fs');

const express = require('express');

let app = express();

app.get('/api/weather', function (req, res) { //针对/api/weather 的 get请求返回

res.writeHead(200, {'Content-Type': 'text/json;charset=utf-8'});

fs.readFile('data.json', 'utf8', (err, data) => { //读取data.json

if(err) { //错误时返回异常

res.end(JSON.stringify({

code : -1,

msg : 'Read File error!'

}));

return;

}

res.end(data);

});

});

app.listen(2333, function(req, res) {

console.log(`You have run node host.`);

});

这样就非常简单,不过缺点就是无法动态地处理。

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×