大多数互联网系统都是分布式部署的,分布式部署确实能带来性能和效率上的提升,但为此,我们就需要多解决一个分布式环境下,数据一致性的问题。 当某个资源在多系统之间,具有共享性的时候,为了保证大家访问这个资源数据是一致的,那么就必须要求在同一时刻只能被一个客户端处理,不能并发的执行,否者就会出现同一时刻有人写有人读,大家访问到的数据就不一致了。 一、我们为什么需要分布式锁?在单机时代,虽然不需要分布式锁,但也面临过类似的问题,只不过在单机的情况下,如果有多个线程要同时访问某个共享资源的时候,我们可以采用线程间加锁的机制,即当某个线程获取到这个资源后,就立即对这个资源进行加锁,当使用完资源之后,再解锁,其它线程就可以接着使用了。例如,在JAVA中,甚至专门提供了一些处理锁机制的一些API(synchronize/Lock等)。 但是到了分布式系统的时代,这种线程之间的锁机制,就没作用了,系统可能会有多份并且部署在不同的机器上,这些资源已经不是在线程之间共享了,而是属于进程之间共享的资源。 因此,为了解决这个问题,我们就必须引入「分布式锁」。 分布式锁,是指在分布式的部署环境下,通过锁机制来让多客户端互斥的对共享资源进行访问。 分布式锁要满足哪些要求呢?
讲完了背景和理论,那我们接下来再看一下分布式锁的具体分类和实际运用。 二、分布式锁的实现方式有哪些?目前主流的有三种,从实现的复杂度上来看,从上往下难度依次增加:
无论哪种方式,其实都不完美,依旧要根据咱们业务的实际场景来选择。
我们先来看一下如何基于「乐观锁」来实现: 乐观锁机制其实就是在数据库表中引入一个版本号(version)字段来实现的。 下面找图举例, (图片来源网络) 如图,假设同一个账户,用户A和用户B都要去进行取款操作,账户的原始余额是2000,用户A要去取1500,用户B要去取1000,如果没有锁机制的话,在并发的情况下,可能会出现余额同时被扣1500和1000,导致最终余额的不正确甚至是负数。但如果这里用到乐观锁机制,当两个用户去数据库中读取余额的时候,除了读取到2000余额以外,还读取了当前的版本号version=1,等用户A或用户B去修改数据库余额的时候,无论谁先操作,都会将版本号加1,即version=2,那么另外一个用户去更新的时候就发现版本号不对,已经变成2了,不是当初读出来时候的1,那么本次更新失败,就得重新去读取最新的数据库余额。 通过上面这个例子可以看出来,使用「乐观锁」机制,必须得满足: 我们再来看一下如何基于「悲观锁」来实现: 悲观锁也叫作排它锁,在Mysql中是基于 for update 来实现加锁的,例如: //锁定的方法-伪代码 上面的示例中,user表中,id是主键,通过 for update 操作,数据库在查询的时候就会给这条记录加上排它锁。 当这条记录加上排它锁之后,其它线程是无法操作这条记录的。 那么,这样的话,我们就可以认为获得了排它锁的这个线程是拥有了分布式锁,然后就可以执行我们想要做的业务逻辑,当逻辑完成之后,再调用上述释放锁的语句即可。
基于Redis实现的锁机制,主要是依赖redis自身的原子操作,例如: SET user_key user_value NX PX 100 redis从2.6.12版本开始,SET命令才支持这些参数: 上述代码示例是指, 为什么这个命令可以帮我们实现锁机制呢? 解锁很简单,只需要删除这个key就可以了,不过删除之前需要判断,这个key对应的value是当初自己设置的那个。 另外,针对redis集群模式的分布式锁,可以采用redis的Redlock机制。
其实基于ZooKeeper,就是使用它的临时有序节点来实现的分布式锁。 原理就是:当某客户端要进行逻辑的加锁时,就在zookeeper上的某个指定节点的目录下,去生成一个唯一的临时有序节点, 然后判断自己是否是这些有序节点中序号最小的一个,如果是,则算是获取了锁。如果不是,则说明没有获取到锁,那么就需要在序列中找到比自己小的那个节点,并对其调用exist()方法,对其注册事件监听,当监听到这个节点被删除了,那就再去判断一次自己当初创建的节点是否变成了序列中最小的。如果是,则获取锁,如果不是,则重复上述步骤。 当释放锁的时候,只需将这个临时节点删除即可。 |
|