etcd简介:
etc是强一致性,高可用的分布式key-value存储系统;主要存储一些分布式系统或集群 使用的数据;
特点:
- 高可用:多etcd节点组成集群
- go语言编写且开源
- raft算法实现 强一致性(C):其算法的选主策略(P),日志复制(C) 保证 多数从节点都同步最新数据后才返回给client; 保证实现CAP原理中的CP; 无法保证A(可用性无法保证,因为网络分区导致数据同步时间不确定)
- key有TTL属性(lease租约):设置key的生命周期
- 支持事务操作
- 支持key的watch功能:当检测到key的值变化时,通知监测的服务
- 客户端与etcd通过grpc及http2.0协议通信:保证多路复用,减少tcp连接数,提高网络通信效率;
- 用户可以通过 命令行/grpc/http restful api(内部转为grpc) 3种方式操作etcd;
- 默认存储2G数据,最多支持8G,再多就会告警;
- etcd集群 在使用 http restful api方式通信时,可以通过 etcd-gateway方式代理 集群中所有节点, 通过访问网关地址来访问etcd集群,使客户端不用关系每个节点的ip变化;
-
etcd集群 在使用 grpc方式通信时,可以通过 grpc-proxy方式 代理 集群中所有节点, 通过访问代理地址来访问etcd集群,使客户端不用关系每个节点的ip变化;
使用场景:
- 分布式锁功能:如 多个相同子系统 接口防并发
- 配置管理:多个业务服务从etcd中获取key对应的value(配置的值)来启动服务;可以通过etcd的watch功能,当配置变化时,对应的业务服务可以实时修改配置,无需重启服务;
- 服务注册/发现: 通过将微服务的ip/port等信息同步到etcd中,其他服务需要时从etcd找到对应的微服务信息; 但是需要考虑 服务的监控问题;
etcd分布式锁功能实现原理:
互斥机制:多个客户端同时尝试获取同一个锁时(同样的key),只有 revision(全局递增)最小的key会获取锁成功;
防止死锁机制:通过 lease(租约) 通过设置ttl(类似redis的超时时间)来实现key的存活时间,到点自动删除; 保证其他服务一定能在固定时间后获取锁;
watch机制:能在key被删除时,通知其他客户端成功获取锁;
etcd分布式锁python版实现代码
通过etcd自带的分布式锁实现 接口级别的分布式互斥锁的装饰器实现
import time
from functools import wraps
# pip install etcd3
import etcd3
from app import fail_response
from app.utils import logger, ApiException
etcd = etcd3.client("127.0.0.1")
def synchronized_api_by_etcd(name=None, expire=None):
""" 通过etcd自带的分布式锁实现 接口级别的分布式互斥锁的装饰器实现
注意事项: 只适用于接口级别
:param name: 锁名称 保证唯一性
Usage:
>>> @api.resource('/test/api')
>>> class TestApiController(BaseController):
>>> '''测试功能
>>> '''
>>>
>>> @synchronized_api_by_etcd(name="dopamine:SYNC:STOCK:MODIFY", expire=3):
>>> def post(self):
>>> pass
"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
""" 对原始函数进行操作
"""
# 重试2次
retry_count = 2
while retry_count:
logger.debug(f"开始尝试对任务'{name}'进行加锁")
try:
# 实现上锁,且设置KEY的TTL为 入参的expire秒
# 实现退出with语句时 删除此锁
with etcd.lock(f"{name}", expire) as lock:
# 获取锁失败,此锁可能被其他程序获取;
if not lock.is_acquired():
logger.debug("获取锁失败,重新尝试")
time.sleep(0.5)
retry_count -= 1
try:
logger.debug(f"任务'{name}'加锁成功,开始执行业务逻辑")
result = func(*args, **kwargs)
except ApiException as e:
result = fail_response(e.code, e.message)
except Exception:
logger.error(f"任务'{name}'执行失败,发生未知异常", exc_info=True)
result = fail_response()
logger.debug(f"任务'{name}'运行完毕,开始释放锁")
return result
except Exception as e:
# 加锁失败,出现异常情况
print(e)
time.sleep(0.5)
retry_count -= 1
logger.debug(f"当前任务'{name}'尝试加锁失败,本任务已在其它位置运行")
return fail_response(500, "无法并发执行")
return wrapper
return decorator
etcd分布式锁go版实现代码
在gin的中间件中实现 接口级分布式锁
//
// @Description: 接口级别的 分布式互斥锁;实现同一时间只能有一个服务提供访问,防并发操作
// @param name: 锁的名字
// @return gin.HandlerFunc:
//
func SynchronizedApi(name string) gin.HandlerFunc {
var l *concurrency.Mutex
synchronizedApiByEtcdLock := func(name string) {
//新建一个lease(租约)
//注意 此租约会一直刷新超时时间(自动续约),保证未手动关闭租约时此租约一直有效,此方式能保证 在后面的获取锁,及释放锁 时间段内 锁能一直有效,最大化防止并发错误;
//所以 不需要用户设置过期时间,只需要写死1s保证最小超时时间即可;这样即使 程序异常退出,1s后可以获取锁,最小化减少锁的影响时间;
//租约退出条件为:直到 手动关闭租约或程序异常退出,租约里的锁会在声明的expire时间后被删除,保证不会死锁;
session, err := concurrency.NewSession(etcd.EtcdCli, concurrency.WithTTL(1))
if err != nil {
glog.Log.Error("获取lease(租约)失败")
panic(err)
}
//最后关闭lease
defer func() { _ = session.Close() }()
//新建一个锁对象
l = concurrency.NewMutex(session, name)
//设置100毫秒后不再尝试获取锁
c1 := context.Background()
c2, cancel := context.WithTimeout(c1, 100*time.Millisecond)
defer cancel()
//尝试上锁
err = l.Lock(c2)
if err != nil {
glog.Log.Info("上锁失败")
panic(api_error.New(504, string("接口不能并发请求")))
}
glog.Log.Info("上锁成功")
time.Sleep(5*time.Second)
}
synchronizedApiByEtcdUnLock := func() {
//开始释放锁
err := l.Unlock(context.TODO())
if err != nil {
glog.Log.Info("删除锁对应的key失败,后续etcd应会自动ttl删除")
panic(err)
}
glog.Log.Info("释放锁成功")
}
return func(c *gin.Context) {
//上锁
synchronizedApiByEtcdLock(name)
//继续执行请求的后续代码
c.Next()
//释放锁
synchronizedApiByEtcdUnLock()
}
}
调用方法
// 路由注册地方调用
apiGroup.POST("/test", SynchronizedApi("projectName/groupName/test"), OtherHandler)
注意:python版无法实现像go版的自动续约功能,只能手动调用 Lock.refresh() 续约;
相关链接: