谈谈 Sniper 框架的数据访问层

2021-01-30 ⏳3.5分钟(1.4千字)

我们在2018年开发了 go-kiss/sniper 框架,并成功驱动了部门的业务系统,为部门的发展奠定了稳固的技术基础。

大约在2019年上半年,我们开放了Sniper的源码。我也在知乎写了两篇文章(一个轻量级 go 业务框架的思考Sniper框架两周年回顾)介绍sniper的一些理念。

Sniper框架在设计的时候就力求简约,一直坚持所谓的 kiss 原则。我们甚至还在 sniper 项目 star 达到 1k 的时候,将仓库迁移到了 go-kiss 这个组织下。

项目发布后肯定引来了很多不同意见。其中讨论比较多的就是为什么没有集成缓存和数据库的支持。今天就聊聊这个问题。

在开始之前,我们先说一下 go 语言的 context。在 go 语言中,起协程非常方便。但如何让协程停止却是一个不小的问题。一个简单的办法就是传入一个 channel,在协程内部通过 select 监听这个 channel 的数据,如果有 channel 有数据,就自行退出。因为这个方法很常用,google 就封装了一个 context 包。然后,各种都库都开始改造,支持传入 context 参数(一般是函数的第一个参数)。

在2018年我们设计sniper的时候,好多库都还不支持传入context,其中就包括gomemcache和go-redis。所以,sniper 在选型的时候首先要选那些支持 ctx 的组件。

另一方面,按我过往的经验,大量依赖 redis 容易产生热 key 和大 key(redis 丰富的数据结构)。当时我们的 redis cluster 还不是很成熟,于是就决定缓存只用 memcached。

最终,我们在 gomemcache 的基础上开发了自己的 memcached 库,并加入了对 context 的支持。因为前期不用 redis,也就没有考虑太多。

后来,我们有了排行榜的需求。不得不引入 redis。那时候的 go-redis 依然不支持 context。于是我们开发了一个简单的 redis 客户端,最核心的当然是加入对 context 的支持。在我们内部,我们只用 redis 做榜单,也就只实现了一部分 redis 接口。

两年过去了。我们使用的 memcached 非常稳定,redis 的适应范围也是日渐扩大。更重要的是 go-redis 已经支持 context 了。而我们现在还得继续维护自己内部的版本。

在 2021 年上半年,我们会把内部的 redis 实现迁移到 go-redis 上。sniper 框架坚持小而美,只写必要的代码。go-redis 够用就用 go-redis。

memcached 虽然也很好,但毕竟用的少了。我们会持续维护自己的版本。

如果是新项目,大家可以直接使用 redis 了,不需要再引入额外的 memcached 依赖。

因为不同团队的情况不一样,要求也就不一样。我们自己的技术造型不一定适合大家(确实不适合),所以,sniper 就没有内置缓存组件。这样大家就可以自由选择而无需纠结了。

以上是缓存的部分。接下来说一下数据库。

数据库用 mysql/mariadb,肯定是要用 database/sql 标准库的。我们在内部,只是对 database/sql 做了很薄的一导封装,主要提供 opentracing、prometheus指标和调试日志的支持。这些功能都是工程实践必须的,但实现的方式可能因人而异,所以就没开放出来。

另一方面,好多朋友提到为什么不用ORM,甚至有人说为什么不用 gorm。现在回头看,没有 orm 确实要写很多 dao 层代码。拼接 sql 确实繁琐。但有一点,拼接 sql 并没有想像中的那样容易出错。拼接 sql 要求程序员对执行什么样的 sql 有完全的了解,反而对查询优化有很大的帮助。

我们也注意到,在实现业务的时候免不了要写一些非标的查询(像 on duplicate、use index 之类的)。与就是说,拼 sql 是避不可少的。

我们还坚持所有的数据库操作只能在 dao 层执行。一定程度上减少了拼 sql 的工作量。

不过,繁琐终归是繁琐。最繁琐的地方莫过于拼接 select 字段和 scan 数据。我们有同学开发了工具,来自动生成相关的代码。我个人现在是倾向于 sqlx 的 ScanStruct 方案(sqlx用了反射,但会缓存相关结果,性能上应该没什么问题。缓存逻辑可以看这里。),不过目前没有获得大家的支持。但不用 orm 这一条我们依然坚持。

好了。总结一下。如果是新项目,我们建议使用 go-redis。我们依然不建议使用 orm。我个人推荐使用 sqlx。以上,给大家一点参考。