1. 字节  B  byte   这个单位是数据里面最重要的一个单位。
  2. 究竟占多大的空间?这是一个很敏感的思考点。

1. 单位转换(对于有些单位换算,换算相差其实是 1000)

1B(byte,字节)= 8 bit;
1KB(Kibibyte,千字节)=1024B= $2^{10}$ B;
1MB(Mebibyte,兆字节,百万字节,简称“兆”)=1024KB= $2^{20}$ B;
1GB(Gigabyte,吉字节,十亿字节,又称“千兆”)=1024MB= $2^{30}$ B;
1TB(Terabyte,万亿字节,太字节)=1024GB= $2^{40}$ B;
1PB(Petabyte,千万亿字节,拍字节)=1024TB= $2^{50}$ B;
1EB(Exabyte,百亿亿字节,艾字节)=1024PB= $2^{60}$ B;
1ZB(Zettabyte,十万亿亿字节,泽字节)=1024EB= $2^{70}$ B;
1YB(Yottabyte,一亿亿亿字节,尧字节)=1024ZB= $2^{80}$ B;
1BB(Brontobyte,一千亿亿亿字节)=1024YB= $2^{90}$ B;
1NB(NonaByte,一百万亿亿亿字节)=1024BB= $2^{100}$ B;
1DB(DoggaByte,十亿亿亿亿字节)=1024NB= $2^{110}$ B;

2. 数据存储与数据传输基本单位:B 与 bit

数据存储是以“字节”(Byte)为基本单位
数据传输是以大多是以“位”(bit,又名“比特”)为基本单位

一个位就代表一个 0 或 1(即二进制),每 8 个位(bit,简写为b)组成一个字节(Byte,简写为B),是最小一级的信息单位。

3. 数据的两种形式转换 比如:0x10000000 = 256MB ?

单位换算注意 进制的区别
0xffffffff 「8位」= ( 1111 1111 1111 1111 1111  1111 1111 1111 )2=  2^32 = 4G

0x1000 0000 =   (268 435 456)10  其实 ,这个数的单位是 B 字节
268 435 456 B = 262 144 KB = 256 MB
(它们的进制是 1024 =  0x400 = 0 0100 0000 0000 = $2^{10}$)

1. 参考博客

图片转 BASE64 编码: https://c.runoob.com/front-end/59/
图片与base64之间的互相转换「Java」: https://blog.csdn.net/weixin_41608476/article/details/79354592

2. 问题描述

在开发一个接口时间,对方接口需要图片的 BASE64 编码。

Base64 是一种基于 64 个可打印字符来表示二进制数据的表示方法。

Base64 常用于在通常处理文本数据的场合,表示、传输、存储一些二进制数据,包括 MIME 的电子邮件及 XML 的一些复杂数据。
图片的 BASE64 编码就是可以将一幅图片数据编码成一串字符串,使用该字符串代替图片地址,从而不需要使用图片的 URL 地址。

当 BASE64 编码的图片 赋值给 String 后,构建失败,提醒常量字符串过长。

3. 探究 String

基础知识:[[../../../1 想法/【Java】2 数据类型#2.3 字符串 String]]
基础知识:[[../../B. 操作系统-Linux/计算机基础知识/数据单位]]

在 Java8 之前,String 内部使用 char 类型数据存储数据, Java9 开始,该用 byte 类型数组来存储数据。所以 String 的最大存储就是数组能存储的最大数量。

数组有两种限制。一是规范隐含的限制。Java 数组的 length 必须是非负的 int,所以它的理论最大值就是 java.lang.Integer.MAX_VALUE = 2^31-1 = 2147483647 位。二是具体的实现带来的限制。这会使得实际的JVM不一定能支持上面说的理论上的最大length。

String 当是一个字符数组。字符是 2字节 16 位 的基本类型,那么一个 String 的最大长度是是 2 G,占用内存是 4GB。

问题

我的字符串肯定没有超过最大限制,但是字符串肯定超过了 2^16。「在IDEA中,字符串长度超过 65535,进行打印,IDEA会提示 java: 常量字符串过长

为什么会提醒我的字符串错过了 65535
引入文章: https://blog.csdn.net/jayzym/article/details/103716868

编译期

当我们使用字符串字面量直接定义 String 的时候,是会把字符串在常量池中存储一份的。那么上面提到的 65534 其实是常量池的限制。
常量池中的每一种数据项也有自己的类型。Java 中的 UTF-8 编码的 Unicode 字符串在常量池中以 CONSTANT_Utf8 类型表示。
CONSTANTUtf8info 是一个 CONSTANTUtf8 类型的常量池数据项,它存储的是一个常量字符串。常量池中的所有字面量几乎都是通过 CONSTANTUtf8info 描述的。CONSTANTUtf8_info的定义如下:

1
2
3
4
5
CONSTANT_Utf8_info {
u1 tag;
u2 length;
u1 bytes[length];
}

我们使用字面量定义的字符串在 class 文件中,是使用 CONSTANTUtf8info 存储的,而 CONSTANTUtf8info 中有 u2 length; 表明了该类型存储数据的长度 u2 是无符号的 16 位整数,因此理论上允许的的最大长度是 2^16=65536。而 java class 文件是使用一种变体 UTF-8 格式来存放字符的,null 值使用两个字节来表示,因此只剩下 65536- 2 = 65534 个字节。

关于这一点,在the class file format spec中也有明确说明:

1
The length of field and method names, field and method descriptors, and other constant string values is limited to 65535 characters by the 16-bit unsigned length item of the CONSTANTUtf8info structure4.4.7). Note that the limit is on the number of bytes in the encoding and not on the number of encoded characters. UTF-8 encodes some characters using two or three bytes. Thus, strings incorporating multibyte characters are further constrained.

也就是说,在Java中,所有需要保存在常量池中的数据,长度最大不能超过 65535,这当然也包括字符串的定义咯。

运行期

运行期的最大长度就是 Integer.MAX_VALUE ,这个值约等于 4G。

4. 解决方案

  1. 使用 StringBuilder 或者 StringBuffer。
  2. 切换 Java 编译器,将 javac 切换为 eclipse 「Eclipse 编译器允许运行没有真正正确编译的代码」

参考博客: https://blog.csdn.net/procrastination/article/details/116056306

大数据量表调整表结构时,有时间会非常慢并且会失败,所以一种比较好的方法是通过类似表修改。

正式环境操作数据一定要当心。

1. 创建需要修改表的类似表

1
CREATE TABLE vehicle_bak like vehicle;

2. 修改类似表的表结构和索引

1
可以对比两个表的查询速度,来确认自己加的索引是否合理。

3. 将 原始表数据 插入到 类似表 中

1
insert into vehicle_bak select * from vehicle;

4. 更改表名

1
2
rename table vehicle to vehicle_1;
rename table vehicle_bak to vehicle;

5. 数据对比,确定未丢数据

1
select count(*) from vehicle;

6. 确认没问题,删除旧表

1
2
truncate table vehicle_bak;
drop table vehicle_bak;

参考博客:MYSQL中取当前周/月/季/年的第一天与最后一天_mysql 取年月的第一天_cleanfield的博客-CSDN博客

1. 方案

1.1. 优化之前

数据统计需要按照 日、月、年 为维度来统计交易流水,SQL 如下,语句看起来很工整但是却存在致命的性能问题。「前提:createdmoney 字段具有联合索引」

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
SELECT sum(money)  
FROM trade
WHERE DATE_FORMAT(created, '%Y-%m-%d') = (SELECT DATE_FORMAT(NOW(), '%Y-%m-%d'));

SELECT sum(money)
FROM trade
WHERE DATE_FORMAT(created, '%Y-%m') = (SELECT DATE_FORMAT(NOW(), '%Y-%m'));

SELECT sum(money)
FROM trade
WHERE DATE_FORMAT(created, '%Y') = (SELECT DATE_FORMAT(NOW(), '%Y'));

SELECT sum(money)
FROM trade
WHERE TO_DAYS(created) = TO_DAYS(NOW());

1.2. 优化思路

created 字段明显存在逻辑的运算,所以查询条件不会走索引,所以更改如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
SELECT sum(money)  
FROM trade
WHERE created >= DATE(DATE_FORMAT(NOW(), '%Y%m%d'))
AND created < DATE_ADD(DATE(DATE_FORMAT(NOW(), '%Y%m%d')), INTERVAL 1 DAY)
# AND DATE(DATE_FORMAT(NOW(), '%Y%m%d')) = DATE_FORMAT(NOW(), '%Y%m%d');

SELECT sum(money)
FROM trade
WHERE created >= DATE_ADD(curdate(), interval -day(curdate()) + 1 day)
AND created < last_day(curdate());

SELECT sum(money)
FROM trade
WHERE created >= DATE_SUB(CURDATE(), INTERVAL dayofyear(now()) - 1 DAY)
AND created < concat(YEAR(now()),'-12-31');

1.3. 结论

目的只有一个,让日期字段不参与到计算当中去。

1.4. 所用到的函数

1
2
3
4
select last_day(curdate());  
--获取当月最后一天。
select DATE_ADD(curdate(), interval - day(curdate()) + 1 day);
--获取本月第一天

2. 有意思的 SQL(同类型)

2.1. SQL

1
2
3
4
5
6
7
8
9
10
SELECT ifnull(sum(o.pay) / 0.945, 0)  
FROM `zx-order`.orders_detail o
LEFT JOIN `zx-order`.goods g ON g.id = o.goods_id
LEFT JOIN `zx-user`.company c ON c.id = g.company_id
LEFT JOIN `zx-user`.user u ON u.id = c.admin_id
WHERE (o.state = 10
OR o.state = 3)
AND DATE_FORMAT(o.pay_time, '%Y%m%d') = DATE_FORMAT(now(), '%Y%m%d')
AND u.au_name = 'XXX'
GROUP BY u.au_name;

2.2. Explain 分析

id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE g index PRIMARY,companyid,idx_goodstype_type companyid 5 70985 100.00 Using where; Using index
1 SIMPLE c eq_ref PRIMARY PRIMARY 4 zx-order.g.company_id 1 100.00
1 SIMPLE u eq_ref PRIMARY,uaccount_anname PRIMARY 4 zx-user.c.admin_id 1 10.00 Using where
1 SIMPLE o ref state,goodsid,idx_state_tenantid_orderno_goodsid,idx_state_paytime_pay_goodsid goodsid 5 zx-order.g.id 70 50.59 Using where

表结构姑且不管,先看索引。 o 表存在字段 statepay_time 的联合索引,o 表字段 goods_idc 表字段 admin_idu 表字段 au_name 均存在索引。

什么都不修改的情况下,响应时间为 24000 ms 左右。

2.3. 初步思路

第一步肯定要修改的查询条件 u.au_name = '薛庆民' ,字段 au_name「 varchar(20) 」,更改为主键查询。通过测试,耗时 1363 ms 左右,虽然快了很多但是还是不满足需求。

ifnull(sum(o.pay) / 0.945, 0) 去除不去效果影响不大。

1
2
3
4
5
6
7
8
9
10
SELECT ifnull(sum(o.pay) / 0.945, 0)  
FROM `zx-order`.orders_detail o
LEFT JOIN `zx-order`.goods g ON g.id = o.goods_id
LEFT JOIN `zx-user`.company c ON c.id = g.company_id
LEFT JOIN `zx-user`.user u ON u.id = c.admin_id
WHERE (o.state = 10
OR o.state = 3)
AND DATE_FORMAT(o.pay_time, '%Y%m%d') = DATE_FORMAT(now(), '%Y%m%d')
AND u.id = 201233
GROUP BY u.id;

思路:优先使用主键来查询,而不是索引,避免回表。

2.3.1. AliYun 数据库治理服务给了一个优化方案。

AND DATE_FORMAT(o.pay_time, '%Y%m%d') = DATE_FORMAT(now(), '%Y%m%d') 替换,把之前日期 = 更改为 <> 的组合,并取中间值。速度一下子减少到只需要 66 ms 左右就可以查询到结果。

1
2
3
4
5
6
7
8
9
10
11
12
SELECT IFNULL(SUM(`o`.`pay`) / 0.945, 0)  
FROM `zx-order`.`orders_detail` `o`
LEFT JOIN `zx-order`.`goods` `g` ON `g`.`id` = `o`.`goods_id`
LEFT JOIN `zx-user`.`company` `c` ON `c`.`id` = `g`.`company_id`
LEFT JOIN `zx-user`.`user` `u` ON `u`.`id` = `c`.`admin_id`
WHERE (`o`.`state` = 10
OR `o`.`state` = 3)
AND `o`.`pay_time` >= DATE(DATE_FORMAT(NOW(), '%Y%m%d'))
AND `o`.`pay_time` < DATE_ADD(DATE(DATE_FORMAT(NOW(), '%Y%m%d')), INTERVAL 1 DAY)
AND DATE(DATE_FORMAT(NOW(), '%Y%m%d')) = DATE_FORMAT(NOW(), '%Y%m%d')
AND `u`.`au_name` = 'XXX'
GROUP BY `u`.`au_name`

思路:按日统计的 SQL 更改为范围,去除字段 pay_time 上的运算,这样子可让字段 pay_time 走索引。

1. 资料

英文官网: https://redis.io/
中文翻译: http://www.redis.cn
书籍:《Redis 实战》Redis实战 — Redis 实战

2. Redis 简介

2.1. 它是什么?为什么使用 Redis?

解决什么问题?方案!对比?

  1. 对于一个系统而言,海量的高并发读是一个很常见的性能问题,缓存是其中的一项解决方法,本质上是空间换取时间。
  2. 缓存, NoSQL「not only sql」,非关系型的数据库。具有数据量大,运行速度快,已被广泛使用。其中 Redis 和 MongoDB 最为广泛。
  3. 使用 Redis 可以很方便的扩展成为一个能够包含数百 GB 数据、每秒处理上百万次请求的系统。
  4. 对于传统使用关系型数据库,比如 MySQL、Oracle 数据库,有些场景它不是很适用,比如商城的秒杀、库存的扣减、首页的访问高峰等等,对于这种高并发很容易将数据库打崩,此时就需要一层缓存中间件。目前常用的 缓存的中间件有 Redis 和 Memcache,但是我只用过 Redis。数据量大,速度。运行速度很快,将其常用数据从数据库一次性查询出来存放在 Redis 中,那么之后大部分的查询只需要基于 Redis 完成便可以了,这样将很大程度上提升网站的性能。

2.2. 它的特点以及附加特性

2.2.1. 特点

  1. 以内存作为存储介质 (内存数据断电会丢失数据)
  2. 存储的数据为持久化,断电或重启后,数据也不会丢失。
    1. 因为 Redis 的存储分为内存存储、磁盘存储和log文件三部分。
  3. 支持主从模式,所以可以配置集群。更利于支撑起大型的项目。
  4. 提供了简单的事务机制,通过事务机制可以有效的保证在高并发的场景下数据的一致性。
  5. 开始增加 Lua 语言的支持,并且 Lua 语言的执行是原子性的。

2.2.2. 特性

  1. 持久化方法:第一种持久化方法为时间点转储,第二种持久化方法将所有修改了数据库的命令都写入一个只追加文件里面。
  2. 故障转移支持。

2.2.3. 性能非常高原因

  1. 内存存储:Redis 是使用内存(in-memeroy)存储,没有磁盘 IO 上的开销。
  2. 单线程实现:Redis 使用单个线程处理请求,避免了多个线程之间线程切换和锁资源争用的开销。
  3. 非阻塞 IO:Redis 使用多路复用 IO 技术,在 poll,epool,kqueue 选择最优IO实现。
  4. 优化的数据结构:Redis 有诸多可以直接应用的优化数据结构的实现,应用层可以直接使用原生的数据结构提升性能。

2.3. 使用 Redis 的理由

2.3.1. Redis 使用场景

在某些场景下,可以充分的利用 Redis 的特性,大大提高效率。这些场景包括缓存,会话缓存,时效性,访问频率,计数器,社交列表,记录用户判定信息,交集、并集和差集,热门列表与排行榜,最新动态等

2.3.2. Redis 热点问题

使用 Redis 做缓存的时候,需要考虑 数据不一致与脏读、缓存更新机制、缓存可用性、缓存服务降级、缓存穿透、缓存预热等缓存使用问题

状态码的职责是当客户端向服务器端发送请求时,描述返回的请求结果。借助状态码,用户可以知道服务器端是正常处理了请求,还是出现了错误。

  1. 目的:客户端判断服务器端是否返回正常
  2. 思考: 通过代码解决代码问题,判断问题分析问题解决问题。

参考博客

  1. 301、404、200、304、500等HTTP状态,代表什么意思?
  2. 维基百科

0. 常见的状态码

200:服务器成功返回网页
404:请求的页面不存在
503:服务器超时

1. 临时响应

1XX 表示临时响应并需要请求者继续执行操作的状态码。

100(继续)请求者应当继续提出请求。服务器返回此代码表示已收到请求的第一部分,正在等待其余部分。

101(切换协议)请求者已要求服务器切换协议,服务器已确认并准备切换。

2. 响应成功

2xx 表示成功处理了请求的状态码。

200(成功)服务器已成功处理了请求。通常,这表示服务器提供了请求的网页。
如果是对您的 robots.txt 文件显示此状态码,则表示 Googlebot 已成功检索到该文件。

201(已创建)请求成功并且服务器创建了新的资源。

202(已接受)服务器已接受请求,但尚未处理。

203(非授权信息)服务器已成功处理了请求,但返回的信息可能来自另一来源。

204(无内容)服务器成功处理了请求,但没有返回任何内容。

205(重置内容)服务器成功处理了请求,但没有返回任何内容。
与 204 响应不同,此响应要求请求者重置文档视图(例如,清除表单内容以输入新内容)。

206(部分内容)服务器成功处理了部分 GET 请求。

3. 重定向

3xx 要完成请求,需要进一步操作。通常,这些状态码用来重定向。

Google 建议您在每次请求中使用重定向不要超过 5 次。您可以使用网站管理员工具查看一下 Googlebot 在抓取重定向网页时是否遇到问题。诊断下的网络抓取页列出了由于重定向错误导致 Googlebot 无法抓取的网址。

300(多种选择)针对请求,服务器可执行多种操作。
服务器可根据请求者 (user agent) 选择一项操作,或提供操作列表供请求者选择。

301(永久移动)请求的网页已永久移动到新位置。
服务器返回此响应(对 GET 或 HEAD 请求的响应)时,会自动将请求者转到新位置。您应使用此代码告诉 Googlebot 某个网页或网站已永久移动到新位置。

302(临时移动)服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来响应以后的请求。
此代码与响应 GET 和 HEAD 请求的 301 代码类似,会自动将请求者转到不同的位置,但您不应使用此代码来告诉 Googlebot 某个网页或网站已经移动,因为 Googlebot 会继续抓取原有位置并编制索引。

303(查看其他位置)请求者应当对不同的位置使用单独的 GET 请求来检索响应时,服务器返回此代码。
对于除 HEAD 之外的所有请求,服务器会自动转到其他位置。

304(未修改)自从上次请求后,请求的网页未修改过。
服务器返回此响应时,不会返回网页内容。

305(使用代理)请求者只能使用代理访问请求的网页。如果服务器返回此响应,还表示请求者应使用代理。

307(临时重定向)服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来响应以后的请求。
此代码与响应 GET 和 HEAD 请求的 301 代码类似,会自动将请求者转到不同的位置,但您不应使用此代码来告诉 Googlebot 某个页面或网站已经移动,因为 Googlebot 会继续抓取原有位置并编制索引。

4. 客户端错误

4xx 这些状态码表示请求可能出错,妨碍了服务器的处理。

400(错误请求)服务器不理解请求的语法。

401(未授权)请求要求身份验证。对于登录后请求的网页,服务器可能返回此响应。

403(禁止)服务器拒绝请求。
如果您在 Googlebot 尝试抓取您网站上的有效网页时看到此状态码(您可以在 Google 网站管理员工具诊断下的网络抓取页面上看到此信息),可能是您的服务器或主机拒绝了 Googlebot 访问。

404(未找到)服务器找不到请求的网页。
例如,对于服务器上不存在的网页经常会返回此代码。

405(方法禁用)禁用请求中指定的方法。

406(不接受)无法使用请求的内容特性响应请求的网页。

407(需要代理授权)此状态码与 401(未授权)类似,但指定请求者应当授权使用代理。
如果服务器返回此响应,还表示请求者应当使用代理。

408(请求超时)服务器等候请求时发生超时。

409(冲突)服务器在完成请求时发生冲突。服务器必须在响应中包含有关冲突的信息。
服务器在响应与前一个请求相冲突的 PUT 请求时可能会返回此代码,以及两个请求的差异列表。

410(已删除)如果请求的资源已永久删除,服务器就会返回此响应。
该代码与 404(未找到)代码类似,但在资源以前存在而现在不存在的情况下,有时会用来替代 404 代码。
如果资源已永久移动,您应使用 301 指定资源的新位置。

411(需要有效长度)服务器不接受不含有效内容长度标头字段的请求。

412(未满足前提条件)服务器未满足请求者在请求中设置的其中一个前提条件。

413(请求实体过大)服务器无法处理请求,因为请求实体过大,超出服务器的处理能力。

414(请求的 URI 过长)请求的 URI(通常为网址)过长,服务器无法处理。

415(不支持的媒体类型)请求的格式不受请求页面的支持。

416(请求范围不符合要求)如果页面无法提供请求的范围,则服务器会返回此状态码。

417(未满足期望值)服务器未满足”期望”请求标头字段的要求。

5. 服务器错误

5xx 这些状态码表示服务器在处理请求时发生内部错误。这些错误可能是服务器本身的错误,而不是请求出错。

500(服务器内部错误)服务器遇到错误,无法完成请求。

501(尚未实施)服务器不具备完成请求的功能。例如,服务器无法识别请求方法时可能会返回此代码。

502(错误网关)服务器作为网关或代理,从上游服务器收到无效响应。

503(服务不可用)服务器目前无法使用(由于超载或停机维护)。通常,这只是暂时状态。

504(网关超时)服务器作为网关或代理,但是没有及时从上游服务器收到请求。

505(HTTP 版本不受支持)服务器不支持请求中所用的 HTTP 协议版本。

来自网络:缓存更新的套路 | 酷 壳 - CoolShell

其实并不只是软件架构里的 MySQL 数据库和 Memcache/Redis 的更新策略,这些东西都是计算机体系结构里的设计,比如 CPU 的缓存,硬盘文件系统中的缓存,硬盘上的缓存,数据库中的缓存。基本上来说,这些缓存更新的设计模式都是非常老古董的,而且历经长时间考验的策略,所以这也就是,工程学上所谓的 Best Practice,遵从就好了。

更新缓存的的 Design Pattern 有四种:

  1. Cache aside
  2. Read through
  3. Write through
  4. Write behind caching

0 错误做法

看到好些人在写更新缓存数据代码时,先删除缓存,然后再更新数据库,而后续的操作会把数据再装载的缓存中。然而,这个是逻辑是错误的。

试想,两个并发操作,一个是更新操作,另一个是查询操作,更新操作删除缓存后,查询操作没有命中缓存,先把老数据读出来后放到缓存中,然后更新操作更新了数据库。于是,在缓存中的数据还是老的数据,导致缓存中的数据是脏的,而且还一直这样脏下去了。

1 Cache Aside Pattern

  1. 失效:应用程序先从 Cache 取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。
  2. 命中:应用程序从 Cache 中取数据,取到后返回。
  3. 更新:先把数据存到数据库中,成功后,再让缓存失效。
    Cache Aside Pattern.png

2 Read / Write Through Pattern

更新数据库(Repository)的操作由缓存自己代理了,所以,对于应用层来说,就简单很多了。
可以理解为,应用认为后端就是一个单一的存储,而存储自己维护自己的 Cache。

  1. Read Through:Read Through 套路就是在查询操作中更新缓存,也就是说,当缓存失效的时候(过期或LRU换出),Cache Aside 是由调用方负责把数据加载入缓存,而 Read Through 则用缓存服务自己来加载,从而对应用方是透明的。
  2. Write Through:Write Through 套路和 Read Through 相仿,不过是在更新数据时发生。当有数据更新的时候,如果没有命中缓存,直接更新数据库,然后返回。如果命中了缓存,则更新缓存,然后再由 Cache 自己更新数据库(这是一个同步操作)。
    ReadWriteThroughPattern.png

4 Write Behind Caching Pattern

Linux 文件系统的 Page Cache 的算法。
在更新数据的时候,只更新缓存,不更新数据库,而我们的缓存会异步地批量更新数据库。

  1. 这个设计的好处就是让数据的 I/O 操作飞快无比(因为直接操作内存嘛 ),因为异步,write backg 还可以合并对同一个数据的多次操作,所以性能的提高是相当可观的。
  2. 但是,其带来的问题是,数据不是强一致性的,而且可能会丢失(我们知道 Unix/Linux 非正常关机会导致数据丢失,就是因为这个事)。
    Write Behind Caching Pattern.png

5 总结

在软件设计上,我们基本上不可能做出一个没有缺陷的设计,就像算法设计中的时间换空间,空间换时间一个道理,有时候,强一致性和高性能,高可用和高性性是有冲突的。软件设计从来都是取舍 Trade-Off。

另外,Write Back 实现逻辑比较复杂,因为他需要 Track 有哪数据是被更新了的,需要刷到持久层上。操作系统的 write back 会在仅当这个 cache 需要失效的时候,才会被真正持久起来,比如,内存不够了,或是进程退出了等情况,这又叫 lazy write。