ole db 访问接口 sqlncli 无法启动分布式事务_分布式锁真的安全吗?

最近工作中遇到了一个非常棘手有趣的故障. 让我结结实实通了两宵,睡了一个周末才缓过来.不过这篇文章讲的并不是这个故障的原因, 而是修复故障所带来的衍生问题和思考.

在升级解决这些机器的过程中, 机器被挂起5~6s.在这段时间中,机器无法响应心跳,并且更重要的是,机器上的时钟发生了滞后, 这对于严格依赖时间偏移的分布式应用的影响是巨大的.也说明了在实际生产中完全有可能出现这个级别的时间紊乱.

对于使用Raft/Paxos的分布式应用,当然这些一致性协议的细节的不同实现可能依赖时间(比如确认当前是否仍是Leader可以通过距离上次心跳的时间来判断),但事实上几乎不受影响.

背景

我总结了下生产中实际使用的分布式锁, 大概有如下几类

单机Redis分布式锁

不同的客户端通过生成随机字符串对制定的Key进行set if not exists + ttl 操作来进行抢占,通过key的TTL时间来决定持有锁的时间. 然后通过LUA脚本执行事务操作进行Compare and delete进行释放锁.

其中的TTL和本机时钟有关.

集群Redis分布式锁

大名鼎鼎Redis作者给出的基于Redis cluster的分布式锁实现,命名为Redlock, 假如一个集群中有5台Redis实例, 那么分别对这5台机器进行上述的单机分布式锁的操作, 操作完成之后判断有几台机器操作成功, 如果成功数量>=3, 则判断所有获取锁的操作时间和TTL的差距, 如果操作时间显著小于TTL, 则认为成功获取锁, 如果不成功, 则反向操作进行CAS释放锁.

Redis作者是超级大牛, 有Redis Cluster这样的作品, 是绝对的分布式专家, 可是给出的这个Redlock算法, 有很多遐想的空间...

数据库锁

  1. 利用insert进行唯一id插入
  2. 利用事务中的select for update 行锁进行操作

这两种实现我只能说非常好用, 尤其是在公司里, 单单是因为分布式锁请其他团队维护一个zookeeper或者redis不是一件很舒心的事情, 唯一的好处是依赖出了问题可以甩锅(逃.

但是DB是每个应用都会用的,另外, 使用DB还有一个额外的优点.后面讲资源服务器的时候会说到.

缺点是, 这两个用起来都比较麻烦, 使用insert的方法需要后台线程进行时间的更新和清理. 避免宕机后无法释放锁.意味着你还需要使用一个线程安全的数据结构和两个线程不停更新db.

使用行锁的方法, 需要关注db的性能, 尤其是压测时整个系统TPS的表现.

ETCD

etcd也支持mini transaction支持多key事务,针对一个key的事务就是cas吗, mini transaction觉得看起来很高级... 这个地方就不多说了, 实现和redis类似, 优点是自带容灾免疫

Zookeeper

这个严格来说, 是我见过生产环境中比较完美的分布式锁, 如果zk没有bug的话, 大家开玩笑说代码一定要比zk更稳定.

实现就是利用zk的临时节点, 然后每个客户端调用zk的递增创建自己的临时节点,然后选出最小的节点即是获得锁的节点.说zk最完美的是因为, 如果客户端失败, 会第一时间释放锁(当然, zk的session是有超时时间的, 这点其实和etcd的lease比较类似)

5a5cb9e3ba62e6150bd27b774cfba1e9.png
etcd的作者说zk的临时节点是一个broken的实现, 我问他为什么, 他并没有回我 QAQ

问题

那么, 这些分布式锁都有一个问题.

正常的流程是 获取锁 -> 执行动作 -> 释放锁. 问题出现在, 在执行动作的时候, 如何得知自己是否仍然持有锁?

因为代码中是否判断是否仍然持有锁依赖的依据是时间. 成功获取锁之后, 发生了长时间的GC, 或者和我遇到的情况一样 整机Hang住了5s, 在这5s的时间里, 其他客户端完全有可能获取到锁, 那么此时将有两个客户端同时获取锁, 如下图所示

6a8daa5a06720b58823575a0e50c3165.png

这种情况下, 锁服务本身是没有问题的, 但是客户端可能会同时获取锁.

针对这种问题, 唯一的解决方案就是, 在真正的资源服务器, 也就是可能发生意外的并发访问的服务器逻辑中, 增加校验, 以保证资源的顺序访问和修改.

以Google的Chubby为例, 提供了如下几种解决方法.

获取锁成功后, 会返回一个包含了lock-id 和递增序列号的Sequencer, 针对不同的诉求级别, 提供了如下三种方式, 级别从严格到宽松

  1. 资源服务器在执行操作前, 请求chubby验证Sequencer是否valid.类似的功能zk也可以做
  2. 资源服务器维护曾经执行过的Sequencer序号, 拒绝小于当前已执行Sequencer的请求
  3. 和其他实现一样, Sequencer具有租约时长限制, 并且在客户端失联(非正常释放锁)后一段时间内禁止其他客户端获取锁

这里面就要说到使用MySQL做分布式锁的巧妙之处了, 如果你使用行锁, 那么你一定在一个事务里, 并且巧妙的是, 如果你访问的共享资源就是当前的DB, 或者你把外部资源的操作状态同步到db中, 那么你始终可以确定的在持有锁的情况下操作共享资源.

后记

  • 分布式锁的应用场景不同, 如果只是想把事情做一次, 并不一定要分布式锁
  • 如果只是一些不那么重要的业务, 比如给用户推送一条营销消息, 漏推或者重复推也可以接受的话, 那么使用一个不那么严格的分布式锁也没有什么问题.
  • 如果仅仅是单一的资源访问, 并且希望使用严格的分布式锁, 需要改动资源服务器的逻辑, 可以考虑直接在资源服务器中实现互斥和顺序访问
  • 如果是选主需求的话, Raft/Paxos最完美, Split-brain? 不存在的
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值