1、引入业务场景
2、分布式锁家族成员介绍
3、分布式锁成员实现原理剖析
4、最后的总结
2019 已经过去!
2020 已经到站!
----
1、引入业务场景
首先来由一个场景引入:
最近老板接了一个大单子,允许在某终端设备安装我们的APP,终端设备厂商日活起码得几十万到百万级别,这个APP也是近期产品根据市场竞品分析设计出来的,几个小码农通宵达旦开发出来的,主要功能是在线购物一站式服务,后台可以给各个商家分配权限,来维护需要售卖的商品信息。
老板大O:谈下来不容易,接下来就是考虑如何吸引终端设备上更多的用户注册上来,如何引导用户购买,这块就交给小P去负责了,需求尽快做,我明天出差!
产品小P:嘿嘿~,眼珠一转儿,很容易就想到了,心里想:“这还不简单,起码在首页搞个活动页... ”。
技术小T:很快了解了产品的需求,目前小J主要负责这块,找了前端和后端同学一起将活动页搞的快差不多了。
业务场景一出现:
因为小T刚接手项目,正在吭哧吭哧对熟悉着代码、部署架构。在看代码过程中发现,下单这块代码可能会出现问题,这可是分布式部署的,如果多个用户同时购买同一个商品,就可能导致商品出现 库存超卖 (数据不一致)
现象,对于这种情况代码中并没有做任何控制。
原来一问才知道,以前他们都是售卖的虚拟商品,没啥库存一说,所以当时没有考虑那么多...
这次不一样啊,这次是售卖的实体商品,那就有库存这么一说了,起码要保证不能超过库存设定的数量吧。
小T大眼对着屏幕,屏住呼吸,还好提前发现了这个问题,赶紧想办法修复,不赚钱还赔钱,老板不得疯了,还想不想干了~
业务场景二出现:
小T下面的一位兄弟正在压测,发现个小问题,因为在终端设备上跟鹅厂有紧密合作,调用他们的接口时需要获取到accesstoken,但是这个accesstoken过期时间是2小时,过期后需要重新获取。
压测时发现当到达过期时间时,日志看刷出来好几个不一样的access_token,因为这个服务也是分布式部署的,多个节点同时发起了第三方接口请求导致。
虽然以最后一次获取的accesstoken为准,也没什么不良副作用,但是会导致多次不必要的对第三方接口的调用,也会短时间内造成accesstoken的 重复无效获取(重复工作)
。
业务场景三出现:
下单完成后,还要通知仓储物流,待用户支付完成,支付回调有可能会将多条订单消息发送到MQ,仓储服务会从MQ消费订单消息,此时就要 保证幂等性
,对订单消息做 去重
处理。
以上便于大家理解为什么要用分布式锁才能解决,勾勒出的几个业务场景。
上面的问题无一例外,都是针对共享资源要求串行化处理,才能保证安全且合理的操作。
用一张图来体验一下:
此时,使用Java提供的Synchronized、ReentrantLock、ReentrantReadWriteLock...,仅能在单个JVM进程内对多线程对共享资源保证线程安全,在分布式系统环境下统统都不好使,心情是不是拔凉呀。
这个问题得请教 分布式锁
家族来支持一下,听说他们家族内有很多成员,每个成员都有这个分布式锁功能,接下来就开始探索一下。
2、分布式锁家族成员介绍
为什么需要分布式锁才能解决?
听听 Martin 大佬们给出的说法:
Martin kleppmann 是英国剑桥大学的分布式系统的研究员,曾经跟 Redis 之父 Antirez 进行过关于 RedLock (Redis里分布式锁的实现算法)是否安全的激烈讨论。
他们讨论了啥,整急眼了?
都能单独写篇文章了
请你自己看 Maritin 博客文章:
https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html
效率:
使用分布式锁可以避免多个客户端重复相同的工作,这些工作会浪费资源。比如用户支付完成后,可能会收到多次短信或邮件提醒。
比如业务场景二,重复获取access_token。
对共享资源的操作是幂等性操作,无论你操作多少次都不会出现不同结果。
本质上就是为了避免对共享资源重复操作,从而提高效率。
正确性:
使用分布式锁同样可以避免锁失效的发生,一旦发生会引起正确性的破坏,可能会导致数据不一致,数据缺失或者其他严重的问题。
比如业务场景一,商品库存超卖问题。
对共享资源的操作是非幂等性操作,多个客户端操作共享资源会导致数据不一致。
分布式锁有哪些特点呢?
以下是分布式锁的一些特点,分布式锁家族成员并不一定都满足这个要求,实现机制不大一样。
互斥性: 分布式锁要保证在多个客户端之间的互斥。
可重入性:同一客户端的相同线程,允许重复多次加锁。
锁超时:和本地锁一样支持锁超时,防止死锁。
非阻塞: 能与 ReentrantLock 一样支持 trylock() 非阻塞方式获得锁。
支持公平锁和非公平锁:公平锁是指按照请求加锁的顺序获得锁,非公平锁真好相反请求加锁是无序的。
分布式锁家族实现者介绍
分布式锁家族实现者一览:
思维导图做了一个简单分类,不一定特别准确,几乎包含了分布式锁各个组件实现者。
下面让他们分别来做下自我介绍:
1、数据库
排它锁(悲观锁):基于 select * from table where xx=yy for update
SQL语句来实现,有很多缺陷,一般不推荐使用,后文介绍。
乐观锁:表中添加一个时间戳或者版本号的字段来实现,update xx set version = new... where id = y and version = old
当更新不成功,客户端重试,重新读取最新的版本号或时间戳,再次尝试更新,类似 CAS
机制,推荐使用。
2、Redis
特点:CAP模型属于AP | 无一致性算法 | 性能好
开发常用,如果你的项目中正好使用了redis,不想引入额外的分布式锁组件,推荐使用。
业界也提供了多个现成好用的框架予以支持分布式锁,比如Redisson、spring-integration-redis、redis自带的setnx命令,推荐直接使用。
另外,可基于redis命令和redis lua支持的原子特性,自行实现分布式锁。
3、Zookeeper
特点:CAP模型属于CP | ZAB一致性算法实现 | 稳定性好
开发常用,如果你的项目中正好使用了zk集群,推荐使用。
业界有Apache Curator框架提供了现成的分布式锁功能,现成的,推荐直接使用。
另外,可基于Zookeeper自身的特性和原生Zookeeper API自行实现分布式锁。
4、其他
Chubby,Google开发的粗粒度分布锁的服务,但是并没有开源,开放出了论文和一些相关文档可以进一步了解,出门百度一下获取文档,不做过多讨论。
Tair,是阿里开源的一个分布式KV存储方案,没有用过,不做过多讨论。
Etcd,CAP模型中属于CP,Raft一致性算法实现,没有用过,不做过多讨论。
Hazelcast,是基于内存的数据网格开源项目,提供弹性可扩展的分布式内存计算,并且被公认是提高应用程序性能和扩展性最好的方案,听上去很牛逼,但是没用过,不做过多讨论。
当然了,上面推荐的常用分布式锁Zookeeper和Redis,使用时还需要根据具体的业务场景,做下权衡,实现功能上都能达到你要的效果,原理上有很大的不同。
*画外音:* 你对哪个熟悉,原理也都了解,hold住,你就用哪个。
3、分布式锁成员实现原理剖析
数据库悲观锁实现
以「悲观的心态」操作资源,无法获得锁成功,就一直阻塞着等待。
1、有一张资源锁表
CREATE TABLE `resource_lock` (
`id` int(4) NOT NULL AUTO_INCREMENT COMMENT '主键',
`resource_name` varchar(64) NOT NULL DEFAULT '' COMMENT '锁定的资源名',
`owner` varchar(64) NOT NULL DEFAULT '' COMMENT '锁拥有者',
`desc` varchar(1024) NOT NULL DEFAULT '备注信息',
`update_time` timestamp NOT NULL DEFAULT '' COMMENT '保存数据时间,自动生成',
PRIMARY KEY (`id`),
UNIQUE KEY `uidx_resource_name` (`resource_name `) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='锁定中的资源';
resource_name 锁资源名称必须有唯一索引。
2、使用姿势