etcd备忘录

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() 续约;

相关链接:

CentOS安装Etcd_当年的春天的博客-CSDN博客_centos etcd

ETCD客户端IDE

python语言etcd3的客户端包

CAP原理在ETCD中的应用

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值