29
f6XF
2936462bda8612e290f17231fddca9a658b472680cb661b537b1121d5b3d683b
f6XF

假设 cpu 核数是 n,总任务数 > n,如果保证运行这些任务的线程不会阻塞,那么只需要启动 n 个线程就够了。压测的结果也验证了这一点,如果开启的线程数 > n,不但没有效果,反而因为线程切换有负面影响。基于这个结果,lealone 的 ClientScheduler 线程和 GlobalScheduler 线程默认都是 cpu 核数。 ​​​

小米花了3年造出第一款车就量产大卖,雷军最早也是撸代码的,哪怕雷军是卷王,汽车和数据库都是很卷的行业,你现在让他花5年造一款新的通用型数据库,他有底气能大卖吗(哪怕只是在国内大卖)?小米花了那么多资源半路参与开源的 hbase,也没见 hbase 多流行,况且 hbase 的难度比通用型数据库低得多。

@zhh-4096

造车复杂,还是造数据库复杂?车和数据库都有一堆零部件组成,现在车企18-24个月就能造出一款新车,主要因为汽车产业链成熟,即便一辆车有上万的零部件,但车企并不会每个零部件都自己造,就是直接用,所以才能在那么短的时间内做出新车并量产。数据库的零部件难通用,24个月难完成,所以数据库更难。 ​​​

造车复杂,还是造数据库复杂?车和数据库都有一堆零部件组成,现在车企18-24个月就能造出一款新车,主要因为汽车产业链成熟,即便一辆车有上万的零部件,但车企并不会每个零部件都自己造,就是直接用,所以才能在那么短的时间内做出新车并量产。数据库的零部件难通用,24个月难完成,所以数据库更难。 ​​​

连续优化了两天,自动根据 jdbc api 动态智能切换 io 模型的最新代码已经提交到 github,压测了一下确实很酷,用户不用关心用什么 io 模型了,参数也不用配。

明白了,时隔多年又踩了一次巨坑,注册 OP_READ 事件和轮询事件必须是同一个线程才不会错乱,我注册事件的线程是应用线程,轮询是客户端调度线程。几年前我在实现多个全局调度器轮流接收新连接时,注册和轮询 ACCEPT 事件也是两个不同线程,也排查了好久,我都把踩坑的注释删了,又踩一次。[摊手]

@zhh-4096

java nio 有个诡异的行为,如果先通过AbstractSelectableChannel.configureBlocking(boolean) 配置成阻塞模式,连接成功后,再配置成非阻塞模式,并注册 SelectionKey.OP_READ,即便后端有发回数据,客户端有时能触发 read 事件,有时又不能。如果先配置成非阻塞模式,之后转成阻塞再转非阻塞都正常。 ​​​

而且明明白白注册好读操作了,同一个事件循环线程刚写完数据,并且已经确认后端数据库成功写回了数据,让这个事件循环线程去读也读不到,返回尝试读多次都读不到。

@zhh-4096

java nio 有个诡异的行为,如果先通过AbstractSelectableChannel.configureBlocking(boolean) 配置成阻塞模式,连接成功后,再配置成非阻塞模式,并注册 SelectionKey.OP_READ,即便后端有发回数据,客户端有时能触发 read 事件,有时又不能。如果先配置成非阻塞模式,之后转成阻塞再转非阻塞都正常。 ​​​

java nio 有个诡异的行为,如果先通过AbstractSelectableChannel.configureBlocking(boolean) 配置成阻塞模式,连接成功后,再配置成非阻塞模式,并注册 SelectionKey.OP_READ,即便后端有发回数据,客户端有时能触发 read 事件,有时又不能。如果先配置成非阻塞模式,之后转成阻塞再转非阻塞都正常。 ​​​

为自己的创新能力骄傲,连 jdbc 客户端都能创新,对比 h2、postgresql、mysql 和 lealone 的 jdbc 客户端已经有了重大区别,前三者依然是最传统的 bio +同步 jdbc api,而 lealone 还额外增加了 nio + 异步 jdbc api,并且把两种模式智能化整合,完美支持高并发和低延迟以及海量 jdbc 连接。 ​​​

终于把客户端的 io 问题和 jdbc 的异步或同步 api 问题完美解决了:

数据库一般部署在内网,bio +专有 tcp 连接解决了80%的场景需求(中低并发低延迟),另外15%的需求通过 nio +专有 tcp 连接解决(高并发但 jdbc 连接数有限制),最后5%的需求通过 nio +共享 tcp 连接解决(高并发且 jdbc 连接数无限制)。

lealone 的默认配置是 bio + 专有 tcp 连接,当应用调用异步 jdbc api 时自动切换到 nio +专有 tcp 连接,当专有 tcp 连接数超过一个阈值时,通常是客户端 cpu 核数的数倍,新创建的 tcp 连接自动使用共享模式,此时在这条共享连接上调用同步或异步 jdbc api 都会委托给客户端调度器线程执行,不再是应用的线程。

通过切换不同的session来实现 bio 或 nio 太复杂了,晚上换了一种思路,通过动态调用 java.nio.channels.spi.AbstractSelectableChannel.configureBlocking(boolean) 来实现居然可行,只要从 bio 转到 nio 时,让 NioEventLoop 线程轮循即可。//@zhh-4096:今天做了一个智能化的改进,用户想选用哪种组合不需要任何配置了,lealone 的 jdbc 客户端会根据调用的 jdbc api 自动选择最优的组合,比如在自动提交模式下哪怕是同一个 Statement 对象,先调用 executeUpdate(sql) 就会自动用 bio,然后调用 executeUpdateAsync(sql) 又自动切换到 nio。收起

@zhh-4096

压测下来最终发现:jdbc 客户端还是使用 bio(阻塞 io)更好,异步 api + nio 才是神仙搭配!

lealone 的 jdbc 客户端有4种组合:

1. bio + 同步 api ;

2. bio + 异步 api ;

3. nio + 同步 api ;

4. nio + 异步 api ;

其中1和2的性能一样;3最慢,比1和2慢20%以上;4最快,高并发时比1-3快10倍。

把 lealone 的 jdbc 客户端从默认的 nio 改成 bio 了,因为 jdbc 本身就是同步风格的 api,应用也习惯了同步风格,bio 配同步 api 最合适。如果应用想追求极致的性能,可以用 lealone 专有的 jdbc 异步 api,然后再搭配 nio,性能马上快10倍,缺点就是要适应异步 api 的编程风格。

2006年我在复习考研,就花了一点时间就差不多把新概念英语前3册背完了,只是觉得考研太无趣,不是考验背诵能力就是考验刷题能力,不符合我的追求,最后我跑去独自研究程序语言和编译器理论了。计算机软件和算法其实更考验逻辑和推理,我搞了10几年数据库,也从没觉得微积分有用过。 ​​​

一个月补课10-12小时花费1200,我一天工作15小时都赚不了这么多,最后试两周(累计3个月),如果效果还一般,我就准备自己出手去辅导侄女了。初中的英语和数学有啥难的,我以前读初中时不是考满分,就是差不多考满分,这两门课都不行,说明文理科都烂。计算机工作要做好,英语和数学是最基础的东西了。

我经常说:与其让 AI 给数据库调参,不如把数据库做得更灵活更动态一点,减少不必要的参数。jdbc 客户端又是一例,当数据库自身能够根据用户调用的 jdbc api 动态选择最快的 io 类型时就达到了完美的状态,都不需要用户调参了,自然用不到 AI。 ​​​

jdbc 或许可以考虑在新版本的规范中引入异步 api 了,需要异步的 api 也就20个左右,lealone 的 jdbc 实现凡是需要异步的 api 都加了 Async 后缀,只要把返回的 Connection 或 Statement 对象强制转换成 JdbcConnection 或 Jdbc Statement 就能用这些异步 api,同步和异步 api 是可以共存的。 ​​​

java 的 nio 也支持阻塞读写的,我之前一直使用老的 jdk bio 库实现阻塞读写,然后我调试这些 jdk bio 库的实现,底层也是调用 nio 的 poll,所以我直接在 nio 之上把阻塞读写功能实现了,性能跟基于老的 jdk bio 库实现的代码一样,最后我把老的 bio 代码全删了,lealone 的 jar 包又小了几 k。 ​​​

今天做了一个智能化的改进,用户想选用哪种组合不需要任何配置了,lealone 的 jdbc 客户端会根据调用的 jdbc api 自动选择最优的组合,比如在自动提交模式下哪怕是同一个 Statement 对象,先调用 executeUpdate(sql) 就会自动用 bio,然后调用 executeUpdateAsync(sql) 又自动切换到 nio。

@zhh-4096

压测下来最终发现:jdbc 客户端还是使用 bio(阻塞 io)更好,异步 api + nio 才是神仙搭配!

lealone 的 jdbc 客户端有4种组合:

1. bio + 同步 api ;

2. bio + 异步 api ;

3. nio + 同步 api ;

4. nio + 异步 api ;

其中1和2的性能一样;3最慢,比1和2慢20%以上;4最快,高并发时比1-3快10倍。

把 lealone 的 jdbc 客户端从默认的 nio 改成 bio 了,因为 jdbc 本身就是同步风格的 api,应用也习惯了同步风格,bio 配同步 api 最合适。如果应用想追求极致的性能,可以用 lealone 专有的 jdbc 异步 api,然后再搭配 nio,性能马上快10倍,缺点就是要适应异步 api 的编程风格。

今天做了一个智能化的改进,用户想选用哪种组合不需要任何配置了,lealone 的 jdbc 客户端会根据调用的 jdbc api 自动选择最优的组合,比如在自动提交模式下哪怕是同一个 Statement 对象,先调用 executeUpdate(sql) 就会自动用 bio,然后调用 executeUpdateAsync(sql) 又自动切换到 nio。

@zhh-4096

压测下来最终发现:jdbc 客户端还是使用 bio(阻塞 io)更好,异步 api + nio 才是神仙搭配!

lealone 的 jdbc 客户端有4种组合:

1. bio + 同步 api ;

2. bio + 异步 api ;

3. nio + 同步 api ;

4. nio + 异步 api ;

其中1和2的性能一样;3最慢,比1和2慢20%以上;4最快,高并发时比1-3快10倍。

把 lealone 的 jdbc 客户端从默认的 nio 改成 bio 了,因为 jdbc 本身就是同步风格的 api,应用也习惯了同步风格,bio 配同步 api 最合适。如果应用想追求极致的性能,可以用 lealone 专有的 jdbc 异步 api,然后再搭配 nio,性能马上快10倍,缺点就是要适应异步 api 的编程风格。

http://t.cn/AX4q2hUc 里面有 testWrite 和 testRead,每个测试4种场景,我把 h2、lealone 实现的 btree 跟 jdk 的 ConcurrentSkipListMap 都测完了,不用任何配置,可以直接跑,h2 在8种场景全面落后,基础数据量是200万个 keyValue。

@zhh-4096

测试了全部8种纯内存读写场景: 单线程或多线程顺序或随机读写,jdk 的 ConcurrentSkipListMap 只有多线程顺序读写这两种场景比 leaone 实现的 btree 略快一点点,其他6种场景都远不如 leaone 的 btree,不管是单线程或多线程,ConcurrentSkipListMap 的随机读写性能都比较差,优势只有多线程顺序读写。 ​​​

测试了全部8种纯内存读写场景: 单线程或多线程顺序或随机读写,jdk 的 ConcurrentSkipListMap 只有多线程顺序读写这两种场景比 leaone 实现的 btree 略快一点点,其他6种场景都远不如 leaone 的 btree,不管是单线程或多线程,ConcurrentSkipListMap 的随机读写性能都比较差,优势只有多线程顺序读写。 ​​​

通过跟5个主流开源数据库的性能对比,我现在可以说 lealone 对数据库领域的最大贡献是:验证了“全局调度器 + 全链路异步化 + CAS 实现的轻量级锁” 这套模型是有效的,能把数据库的并发处理能力提升到新的高度。希望未来能有更多数据库也采用类似的模型。 ​​​

不管是单线程还是多线程,sqlite 的各类查询操作的性能不算很差,lealone 只比 sqlite 快2-3倍;sqlite 的 insert/update/delete 这类写操作的性能确实差很多,lealone 基本比 sqlite 快10-40倍。sqlite 的优势是用 c 语言实现,确实比 java 更适合内存有限的嵌入式场景。 ​​​

嵌入式场景:

lealone 的主要竞争对手是 h2 和 sqlite;

client-server 场景:

lealone 的主要竞争对手是 mysql 和 postgresql。

所以我只重点压测 h2、sqlite、mysql 和 postgresql。 ​​​

问了一下豆包: sqlite 如何实现单行记录的更新 http://t.cn/AX4hoQtk 它说 sqlite 不支持行锁,如果真的不支持行锁,那就不可能快了。

@zhh-4096

继续优化 lealone 8.0 在嵌入式场景下的性能,16个并发,按主键随机更新单行记录,lealone 比 h2 快8倍,比 sqlite 快20倍。 ​​​

为啥 lealone 更新单行记录的性能这么好?只需要遍历一次 btree,拿到记录后通过 CAS 加行锁,替换新值,然后标记记录所在的 btree page 为脏页即可。而 h2 至少要遍历3次 btree,如果并发高了,还可能需要对 btree 加全局锁。sqlite 的实现我不是很清楚,所有类型的并发写都很弱,insert 更慢。

@zhh-4096

继续优化 lealone 8.0 在嵌入式场景下的性能,16个并发,按主键随机更新单行记录,lealone 比 h2 快8倍,比 sqlite 快20倍。 ​​​

广西怎么没有桂超?都忙着坎甘蔗摘沙糖桔了。贵州的村超很火,其实在我小时候桂林的村超就火过了,好多农村都新建了篮球场,村与村之间晚上经常打比赛,看的人围了里三层外三层。现在再去农村,篮球场早就荒废了。哪怕是春节,娱乐活动变成打牌赌钱或刷抖音。 ​​​

在抖音上刷到湘超半决赛,隔壁的永州居然打败长沙进了决赛,两个进球还挺漂亮。永州就是个毫无存在感的地方,这种地级市足球队踢球的水平和观赏性怎么比国家队还高,说明足球圈还是人才济济的。我不是足球迷,也搞不懂国家队足球运动员的选拔标准。 ​​​

把 oltp 数据库常用的功能都压测了一遍,哪怕是创建连接的速度都压测了,mysql 从5.7到9.5版本的性能没有一个场景胜过 lealone 8.0,哪怕打平的场景都没有,万万没想到 mysql 已经这么差劲了! ​​​

单线程对连续的一万条记录做 count 和 sum 运算,这种功能没有任何技术难度,在我看来不同的 oltp 数据库对于这类功能也难做出差距,但 mysql 居然比 postgresql 和 lealone 慢2-3倍,难以置信! ​​​

在 localhost 创建一条 jdbc connection 的速度:

postgresql 要58毫秒(因为它创建进程);

mysql 要14毫秒;

h2 要2毫秒;

lealone 最快能做到60微秒(注意是微秒)。 ​​​

高并发场景,只要有连续不断的 sql 要处理,lealone 的异步客户端的性能已经很接近批量执行了。线上 oltp 场景要执行的 sql 是不同的,所以不太可能让应用自己攒够一批 sql 再通过 jdbc 的 executeBatch 去做,这时通过 lealone 的 executeUpdateAsync 和 executeQueryAsync 也能接近批量执行的性能。