背景
笔者近期参加了一个答辩,所有参与答辩的组完成的都是同一个项目。通过整理和思考评委对我们组和其他组的点评以及修改意见,我有了很大的收获。
项目的大致情况
所有组通用的技术选型:
- 语言:Go
- 数据库:Gorm
- 技术框架:多见HTTP框架hertz、RPC框架kitex
- 缓存:Redis
我们组: 外加CloudWeGo生态的一些中间件;HBase;Kafka
问题整理
安全性方面
建议
- 数据库中存储的密码信息不要明文存储
- JWT可被反解,只能用于放些简单数据
问题
- 如何防止SQL注入? 答:Gorm有机制。
评委老师说不要过度依赖机制。
- 鉴权如何完成? 是token校验相关的问题。
业务逻辑设计+组件使用注意事项
建议(开发约定)
- db count 大容量时会有性能风险
- 使用Redis时建议采用冷热分离策略
- Redis可能会挂掉,不要用它作为唯一存储
- 分库分表的对象要考虑清楚
- 单个文件的代码最好别超80行
- 线上环境不建议使用外键
(笔者认为这里指的是物理外键;对于事实上确有关系的表,可写成逻辑外键)
- 不建议在事务中做RPC请求,可能会导致死锁或者超时且成功的情况
If both the RPC request and its response are to be contained within the transaction, and if some part of the transaction depends on the result returned by the response, then it is not possible to use the memory-buffer tricks that can be used in the case of buffered I/O. Any attempt to take this buffering approach would deadlock the transaction, as the request could not be transmitted until the transaction was guaranteed to succeed, but the transaction's success might not be knowable until after the response is received.
- 数据库尽量使用软删除
A soft delete marks a record as no longer active or valid without actually deleting it from the database.
- 如无必要,不加实体。加入中间件后会引入一些额外的复杂度。
- 当项目中的rpc响应体字段碰巧和http的字段一样时,也不要直接返回rpc的响应体字段
- 不建议使用hbase作为线上服务 原因:会有一些抖动的情况,比如耗时突然增加比较多;这是由于它的底层代码中的大k之类的有问题。
提问
了解zset的大key问题吗?多少数据量会触发?
BigKey含有较大数据或含有大量成员、列表数的Key。
我看的文章中都没有提到这个阈值,在阿里云的一篇文章中举了一个例子:
- 一个ZSET类型的Key,它的成员数量为10000个(成员数量过多) 其中指出,这个阈值并不是一个确定的标准,而是根据不同的数据结构、具体的业务场景来考虑。
Redis有考虑过缓存雪崩、缓存击穿、缓存穿透的问题吗?是如何考虑的?
缓存雪崩
- 现象:Redis不起作用,大量请求直接打到数据库上
- 原因:
- Redis中的大量数据同时过期,无法在Redis中找到数据,只能去下一级的数据库找;
- Redis故障宕机,无法工作,只能访问下一级的数据库
- 解决方法:
针对数据过期:
- 给数据的过期时间加上一个随机数,让它们不会同时过期
- 一个请求访问Redis时,如果发现它要的数据不在Redis中,加个互斥锁,等它从数据库读取完数据并更新后再释放锁,保证同一时间只有一个请求来构建缓存;为了避免出现死锁,即一个进程一直占有锁而其他进程一直等待的情况,设置一个超时时间。
- 双key策略。两个key的value是一样的,只是主key设置过期时间,从key没有(相当于一个副本)。如果不能访问主key就去访问它的副本。但是更新的时候是主副一起更新。
- 缓存不设置有效期,并由后台线程定时更新。由于不设置有效期不代表它能一直呆在内存(系统内存资源紧张的时候还是会换出一些数据的),针对从这到后台线程更新的时间间隔中的数据丢失情况的解决方案:
- 后台线程频繁检测缓存是否有效并更新
- 业务线程检测到数据丢失后,通过消息队列发送消息通知后台线程更新(该方式更及时)
针对Redis宕机:
- 服务熔断
暂停所有业务对数据库的访问,在请求Redis时就已经返回错误,让其也没机会访问数据库(缓兵之计)
- 限流
暂停所有业务访问会对业务造成很大的影响,为了减少这种影响,可以只允许一部分访问,这就是限流,它减轻了数据库的压力;等数据库恢复正常之后再恢复(一种缓兵之计,真正的解决措施在别处)
- 通过主从节点构建高可用集群(未雨绸缪)
缓存击穿(cache invalid)
- 现象:某个会被频繁访问的数据(热数据)过期,无法在缓存访问到,大量请求直接打在数据库上。
- 解决方案:和上面类似:加锁/不设置过期时间,而由后台进程定时更新
缓存穿透
- 现象:用户访问的数据既不在缓存中,也不在数据库中。
- 原因:
- 误操作删除了很多有用的数据
- 黑客恶意访问不存在的数据
- 解决方案:
- 限制非法请求
- 先设布隆过滤器判断数据是否存在,而不是直接请求数据库
- 针对已经出现的查空请求,在缓存设置对应空值或默认值,使后续请求可以返回该值
Redis操作如何保证原子性?
- 使用单命令操作。如自增 自减
- 把多个操作写到一个
Lua脚本中,以原子性方式执行单个Lua脚本
Redis操作如何保证数据一致性?
所谓的数据一致性,是指要保持缓存和数据库中的数据同步。而数据不一致归根结底是由于并发操作引起的。
先更新数据库,再删除缓存中的数据
- 解决方案:更新时 先更新数据库,再删除缓存中的数据
- 采用这个先后顺序的原因是:在缓存中的耗时远比在数据库中的要短,出现删除缓存后另一个请求重写旧的缓存值的几率很小,它最大程度地保证了一致性。
- 可能存在的问题:当第二步删除缓存失败时,会导致数据更新丢失。解决思路就是:异步操作缓存。具体方案有两个:
- 使用 消息队列 来 重试 删除缓存
- 先订阅binlog,再删除缓存 (比如canal通过模拟主从复制协议,伪装成MySQL的从节点,向主节点发送dump请求,以获取binlog中的变更日志)
笔者补充:还有其他的方案吗?讲讲它的具体操作和优缺点?
其他不同的方案依然是围绕数据库和缓存两方面展开。
先删缓存,再更新数据库
比如:先删除缓存,再更新数据库。相应解决不一致的方案是:延迟双删,步骤如下:
- 先删缓存
- 再更新数据库
- 睡眠状态
- 再删缓存
这种方法的出发点是希望在一个请求的睡眠时间内,另一个并发的请求已经完成了读数据库并且写回缓存的操作。
但不确定性在于这个睡眠时间到底要设多少。极端情况下还是会出问题。
更新数据库和更新缓存
因为删除缓存会影响命中率,如果业务对命中率要求比较高的话,可以考虑这种方案。
可以采用以下方法来保证一致性:
- 更新缓存前加入分布式锁
- 更新缓存后设置过期时间
对索引的使用是怎么考虑的?
用唯一索引。它的查找效率比较高。
什么情况下才需要考虑分库分表
一般情况下不需要分库分表,避免过度设计和过早优化。 不过,
当数据库遇到瓶颈的时候可以考虑,大致分为两种情况:
CPU瓶颈
而这也可以再细分:
- 如果是SQL导致的,比如用了join、group by、order by等等,可以通过用适当的索引,并在service层进行拼接操作。
- 如果是单表数据量太大,就要考虑水平分表。
所谓的水平分表,就是以字段为依据,按照一定策略(hash、range等),将一个表中的数据拆分到多个表中。(也就是按行分)
IO瓶颈
- 如果是磁盘读I/O瓶颈,请求量太大,缓存没有那么多空间装热点数据,导致直接打到数据库上的请求多,这种情况可以考虑分库和垂直分表。(垂直分表也就是按列分)
- 网络I/O瓶颈,请求数据太多,网络带宽不够,可以考虑分库。
hbase不好用,那用什么?
据说可以试试阿里云的Lindorm
kafka发消息也有可能失败,如何保证一致性?
In “at-least-once” delivery, Kafka guarantees that all messages will be delivered at least one time—and possibly more, depending on how many times the producer tries to send the message.
A stricter guarantee is “exactly-once” delivery in Kafka, which guarantees that all messages will be delivered only one time. Distributed event processing systems can use Kafka’s “exactly-once” delivery to assure that the system’s property of eventual consistency will be preserved.
Behind the scenes, “exactly-once” delivery in Kafka is implemented with a two-phase commit (2PC) protocol.
Each read/process/write step in Kafka is appended to a log using an idempotent operation (i.e. the append will happen only once even if the operation is executed multiple times).
The system can then check these logs to see if there are any pending transactions that need to be finished.
简单翻译一下:
- 在at least once delivery中,消息可以多次发送
- 在exactly once delivery中,采用了 两阶段提交 协议。Kafka 中的每个读/处理/写入步骤都使用幂等操作附加到日志中(即,即使多次执行操作,追加也只会发生一次)。然后,系统可以检查这些日志,以查看是否有任何需要完成的待处理事务。