etc是强一致性,高可用的分布式key-value存储系统;主要存储一些分布式系统或集群 使用的数据;
etcd集群 在使用 grpc方式通信时,可以通过 grpc-proxy方式 代理 集群中所有节点, 通过访问代理地址来访问etcd集群,使客户端不用关系每个节点的ip变化;
互斥机制:多个客户端同时尝试获取同一个锁时(同样的key),只有 revision(全局递增)最小的key会获取锁成功;
防止死锁机制:通过 lease(租约) 通过设置ttl(类似redis的超时时间)来实现key的存活时间,到点自动删除; 保证其他服务一定能在固定时间后获取锁;
watch机制:能在key被删除时,通知其他客户端成功获取锁;
通过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
在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() 续约;
相关链接: