林淮川 毕业于西安交通大学,现任大树金融架构师,技术委员会委员;前大树金融供应链金融技术总监;前天阳宏业交易事业部技术主管。5 年互联网金融行业业务经验。多次主导金融服务平台的设计、策划、实施与交付。拥有丰富的大型软件平台构架设计经验,以及供应链金融业务经验。
孙玄:毕业于浙江大学,现任转转公司首席架构师,技术委员会主席,大中后台技术负责人(交易平台、基础服务、智能客服、基础架构、智能运维、数据库、安全、IT 等方向);前58集团技术委员会主席,高级系统架构师;前百度资深研发工程师;
【架构之美】微信公众号作者;擅长系统架构设计,大数据,运维、机器学习等技术领域;代表公司多次在业界顶级技术大会 CIO 峰会、Artificial、Intelligence、Conference、A2M、QCon、ArchSummit、SACC、SDCC、CCTC、DTCC、Top100、Strata+、Hadoop World、WOT、GITC、GIAC、TID等发表演讲,并为《程序员》杂志撰稿 2 篇。
提到分布式锁,有很多实现,比如 Redis 分布式锁、ZooKeeper 分布式锁、etcd分布式锁等。但是选择哪个更适合你的项目?在《基于CAP模型设计企业级真正高可用的分布式锁》一文深入分析过分布式锁的哲学本质,以及如何结合场景来选择合适的分布式锁。分析业务场景,得到业务本质,就是架构思维。思维最终是需要落地的,接下去分享一下对分布式锁的思考和实践。
锁的本质是对共享资源的处理,表现很多,有以下作用:
在单体应用时代表现为同步块 lock。随着需求和业务量的增长,系统走向了分布式、微服务时代,多服务和多实例下的应用无法使用本地锁进行控制资源共享。此时就出现了分布式锁,分布式场景下对分布式锁的要求如下:
目前常见分布式锁的实现有 Redis、ZooKeeper、etcd 等,各维度指标对比如下:
一致性算法(CAP):在分布式场景下,CAP 理论是很多架构设计的指导思想。CAP 思想下有两个分支 CP 与 AP;CP 模型不管什么情况下,都要求各服务之间的数据一致;AP 模型高可用下的数据最终一致性。虽然锁原本要求强一致性 CP模型,但 AP 模型分布式锁的使用取决于业务场景对脏数据的最大容忍度,比如SNS 场景,就可以使用AP模型分布式锁,从而在性能上有很大的优势。CP 模型仍然保持原有的一致性要求,保证了业务资源串行竞争,更加适合于金融交易场景的强数据要求。Redis 自身无一致性算法来保证多节点的数据一致性,所以是 AP 模型;ZooKeeper、etcd 都有一致性算法,都是 CP 模式。
高可用:Redis 是一个 K-V 存储,使用主从模式进行集群,Redis Cluster 底层也是主从模式的组合,性能高,保证了高可用。ZooKeeper 是 Tree 的数据结构,节点要求 N + 1,N 必须大于 2,通过 ZAB 选举保障主的可用。etcd 是一个 K-V 存储,节点要求 N + 1,N 必须大于2,通过 Raft 选举保障主的高可用。
根据需求,设计出锁接口,首先锁的基本方法如下:
/**
* @方法名称 lock
* @功能描述 获取锁
* @param ttl 锁过期时间,单位毫秒
* @return true-获取锁,false-为获得锁
* @throws RuntimeException 操作锁失败,需要业务判断是否重试
*/
boolean lock(int ttl) throws RuntimeException;
第二,分布式锁可以处理业务幂等,可用作为消息去重等场景,设计竞争锁方法如下:
/**
* @方法名称 acquire
* @功能描述 竞争锁,并自动续租
* @param ttl 锁过期时间,单位毫秒
* @return true-获取锁,false-为获得锁
* @throws RuntimeException 操作锁失败,需要业务判断是否重试
*/
default boolean acquire(int ttl)
throws RuntimeException {
if (lock(ttl)) {
logger.debug(MSG_LOCK, getName());
startHeartBeatThread();
return true;
}
return false;
}
第三,作为锁的基本要求,业务的串行执行,设计等待锁方法如下:
/**
* @方法名称acquireOrWait
* @功能描述 竞争锁或等待锁
* @param ttl 锁过期时间,单位毫秒
* @param waitTime 等待时间,单位毫秒
* @return true-获取锁,false-为获得锁
* @throws InterruptedException
* @throws RuntimeException 操作锁失败,需要业务判断是否重试
*/
default boolean acquireOrWait(int ttl, int waitTime)
throws InterruptedException,RuntimeException {
while (!lock(ttl)) {
waitTime =waitTime - ttl / 2;
Thread.sleep(ttl / 2);
if (waitTime<= 0) {
logger.debug(MSG_LOCK_TIMEOUT, getName());
return false;
}
}
startHeartBeatThread();
return true;
}
第四,分布式场景,锁需要自动续租方法,保障锁内业务完整执行,如下:
/**
* @方法名称startHeartBeatThread
* @功能描述 续租心跳
*/
void startHeartBeatThread();
第五,锁需要自动释放,为保证使用简单,所以重写 Closeable 接口:
/**
* @方法名称 close
* @功能描述 释放锁
*/
@Override
void close();
/**
* @方法名称 release
* @功能描述 释放锁
*/
default void release() {
close();
}
etcd 有 V2 和 V3 两种接口:V2 接口可以使用 http 直接访问,天然客户端物理解耦,但需要自动续租保证锁的完整性。V3 接口默认 grpc 形式,是长链接机制,天然续租,但 grpc 有客户端依赖要求。可以根据场景要求,适度选择合适版本接口。
锁参数有:
Linux curl 锁操作:
全部源代码请访问以下链接:
https://github.com/linhuaichuan/ecp-uid/tree/master/src/test/java/com/myzmds/ecp/core/standard/distributed/lock