上下文
传统 Python 单机系统部署中,由于 GIL 的存在,相同进程中我们可以不用处理并发问题。但是随着业务发展需要,原有单机系统演变成分布式或多进程系统后。这将使原有的单机单进程并发控制策略失效。为了解决该问题需要引入一种跨进程、跨机器的互斥锁机制来控制共享资源的访问,这也就是分布式锁的由来。
所以,分布式锁的引入是为了保障多台机器或多个进程对共享资源读写的同步,保证数据的最终一致性。
分布式锁天生具有如下特点:
- 互斥性:任何时刻,只允许单客户端(单进程、单节点)能持有锁。
- 安全性:能有效避免死锁情况,即使客户端持有锁机器发生崩溃或退出故障,也需要保障锁能被正确释放。
- 可用性:具备高可用能力,锁服务节点故障(即使宕机)也不应影响服务的正常运行。
- 可重入性:针对同一个锁,加锁和解锁必须为同一个进程,即对称性。
常见分布式锁实现方式:
- 数据库:采用乐观锁、悲观锁或主键 ID 实现。
- 缓存:采用 Redis 或 RedLock (Redis组件) 实现。
- 一致性算法:采用 Zookeeper、Chubby 或 Etcd 实现。
以上,号主知道的常见实现大致就这些了。接下来的篇幅主要讲解如何使用 Etcd 实现分布式锁业务,我们往下看。
基于 ETCD 的分布式锁实现
环境安装
为了保证锁服务可用性
,我们搭建一个包含三个 Etcd 节点的 docker 集群环境,此处通过docker-compose
进行实验配置。熟悉部署安装的号友,可跳过此部分。
「步骤一:镜像拉取」
代码语言:javascript复制docker pull bitnami/etcd:3.5.2
「步骤二:编辑docker-compose.yml」
代码语言:javascript复制version: '3.5'
services:
etcd1:
container_name: builder-etcd1
image: bitnami/etcd:3.5.2
ports:
- 12379:2379
environment:
- ALLOW_NONE_AUTHENTICATION=yes
- ETCD_AUTO_COMPACTION_MODE=revision
- ETCD_AUTO_COMPACTION_RETENTION=1002
- ETCD_QUOTA_BACKEND_BYTES=4294967296
- ETCD_NAME=etcd1
- ETCD_INITIAL_ADVERTISE_PEER_URLS=http://etcd1:2380
- ETCD_LISTEN_PEER_URLS=http://0.0.0.0:2380
- ETCD_LISTEN_CLIENT_URLS=http://0.0.0.0:2379
- ETCD_ADVERTISE_CLIENT_URLS=http://etcd1:2379
- ETCD_INITIAL_CLUSTER_TOKEN=etcd-cluster
- ETCD_INITIAL_CLUSTER=etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380
- ETCD_INITIAL_CLUSTER_STATE=new
volumes:
- ${DOCKER_ROOT_DIR:-.}/volumes/etcd/data1:/bitnami/etcd
etcd2:
container_name: builder-etcd2
image: bitnami/etcd:3.5.2
ports:
- 22379:2379
environment:
- ALLOW_NONE_AUTHENTICATION=yes
- ETCD_AUTO_COMPACTION_MODE=revision
- ETCD_AUTO_COMPACTION_RETENTION=1002
- ETCD_QUOTA_BACKEND_BYTES=4294967296
- ETCD_NAME=etcd2
- ETCD_INITIAL_ADVERTISE_PEER_URLS=http://etcd2:2380
- ETCD_LISTEN_PEER_URLS=http://0.0.0.0:2380
- ETCD_LISTEN_CLIENT_URLS=http://0.0.0.0:2379
- ETCD_ADVERTISE_CLIENT_URLS=http://etcd2:2379
- ETCD_INITIAL_CLUSTER_TOKEN=etcd-cluster
- ETCD_INITIAL_CLUSTER=etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380
- ETCD_INITIAL_CLUSTER_STATE=new
volumes:
- ${DOCKER_ROOT_DIR:-.}/volumes/etcd/data2:/bitnami/etcd
etcd3:
container_name: builder-etcd3
image: bitnami/etcd:3.5.2
ports:
- 32379:2379
environment:
- ALLOW_NONE_AUTHENTICATION=yes
- ETCD_AUTO_COMPACTION_MODE=revision
- ETCD_AUTO_COMPACTION_RETENTION=1002
- ETCD_QUOTA_BACKEND_BYTES=4294967296
- ETCD_NAME=etcd3
- ETCD_INITIAL_ADVERTISE_PEER_URLS=http://etcd3:2380
- ETCD_LISTEN_PEER_URLS=http://0.0.0.0:2380
- ETCD_LISTEN_CLIENT_URLS=http://0.0.0.0:2379
- ETCD_ADVERTISE_CLIENT_URLS=http://etcd3:2379
- ETCD_INITIAL_CLUSTER_TOKEN=etcd-cluster
- ETCD_INITIAL_CLUSTER=etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380
- ETCD_INITIAL_CLUSTER_STATE=new
volumes:
- ${DOCKER_ROOT_DIR:-.}/volumes/etcd/data3:/bitnami/etcd
networks:
default:
name: builder_dev
部署配置文件 docker-compose.yml 下载,详见 [1]
「步骤三:启动服务」
代码语言:javascript复制docker-compose -f docker-compose.yml up
「步骤四:验证集群」
验证集群节点的版本
代码语言:javascript复制docker exec -it builder-etcd1 /bin/bash -c "etcd --version"
docker exec -it builder-etcd2 /bin/bash -c "etcd --version"
docker exec -it builder-etcd3 /bin/bash -c "etcd --version"
# 输出
etcd Version: 3.5.2
Git SHA: 99018a77b
Go Version: go1.16.3
Go OS/Arch: linux/amd64
验证三个节点返回的数据一致
代码语言:javascript复制docker exec -it builder-etcd1 /bin/bash -c "etcdctl put greeting "hello, etcd""
# OK
docker exec -it builder-etcd1 /bin/bash -c "etcdctl get greeting"
docker exec -it builder-etcd2 /bin/bash -c "etcdctl get greeting"
docker exec -it builder-etcd3 /bin/bash -c "etcdctl get greeting"
# 输出
greeting
hello, etcd
锁的实现基础机制
此处为 Etcd 核心机制,下文讲解的分布式锁就是基于这些基础功能和基础特性实现。
「Lease 机制」:即租约机制(TTL),Etcd 可以为存储的 kv 对设置租约,当租约到期,kv 将失效删除;当然也支持 refresh 续约。
「Revision 机制」:存储的每个 key 带有一个 Revision 属性值,Etcd 每进行一次事务操作,对应的全局 Revision 值都会加一,因此每个 key 对应的 Revision 属性值都是全局唯一的。通过比较 Revision 的大小就能知道写操作的顺序。
「公平锁机制」:多个程序同时抢锁时,会根据 Revision 值大小依次获得锁,可以有效避免 “惊群效应”,公平获取。
「Prefix 机制」:即前缀机制,可以根据前缀获取该目录下所有的 key 及对应的属性(包括 key, value 以及 revision 等)。
「Watch 机制」:即监听机制,Watch 机制支持 Watch 某个固定的 key,也支持 Watch 一个目录(前缀机制),当被 Watch 的 key 或目录发生变化,客户端将收到通知(