这篇文章也是我对分布式锁认知的一个总结吧,目前所做的流量工程使用到了基于Redis的分布式锁,由此延伸到其他的概念及应用

什么是分布式锁

提到锁,Java开发者的第一印象肯定会想到Java线程中的锁,基于Java的内存模型:每个线程有自己的内存空间,同时这些线程有个共享的主内存,不同线程先更新自身内存再更新主内存,在更新主内存的同一条数据的时候,需要加锁控制。多线程开发中常见锁的关键字有synchronized、ReentrantLock等,但多线程锁是存在一个JVM之中的,如果操作的数据不在一个JVM中,多线程中锁就失效了,这种情况下分布式锁就诞生了,即多个Java实例、甚至不一定是Java程序、或多个系统需要操作同一个副本数据的时候,需要一个指挥交通的人指定操作的先后顺序,这就是分布式锁的概念。

什么场景下需要分布式锁

单纯将分布式锁的概念可能有点抽象,以流量业务场景中例子来说明,手机用户可以在手机App端、网上营业厅、wap手厅进行流量业务的操作,如果发现该用户没有流量账户的时候,会首先给该用户创建一个专门的流量账户,如果用户在app端、网上营业厅端同时操作的时候,可能会给该用户创建2个账户;再举一个例子,一个公共集团账户,下面包含很多账户,给下面账户充值的时候,会对该集团账本进行资金扣减,高并发多请求的时候会到导致并发失败,这时候为了减少失败率,提升QPS/TPS,同样需要分布式锁

行业内解决方案

上面说到了扣减同一账本的时候,估计就可以想到,其实这可以用乐观锁悲观锁来实现,确实,广义上来讲,乐观锁悲观锁也是分布式锁,这是一种基于数据库实现的分布式锁,另外还有其他多种方案,比如基于ZooKeeper实现分布式锁、基于Redis实现分布式锁(我们项目中就是使用该方式),下面一个一个分析:

  • 1、基于数据库悲观锁乐观锁
    a)悲观锁:每次拿到数据的时候都认为数据是被人改过的或有可能被更改,因为每次都进行加锁,最直接的方式:
    1
    select * from table where (...) for update

b)乐观锁:每次去拿数据的时候都认为别人不会修改,但是更新的时候会判断有没有其他操作更新了该数据,根据比较版本号(或时间戳)的方式来衡量当前版本是不是最新版本:

1
2
select version from table
update table set version=version+1 where version=?

  • 2、基于Redis实现
    Redis版本的实现也是因为不管单机还是集群分布式,都需要从同一个地方获取锁,而单线程的Redis恰巧具备这样的特性,再加上合理的命令,自然可以实现分布式锁的功能。当然,即使使用Redis实现,也有不同的实现逻辑,下面依次介绍:
    a)set+expire(细节待补充)
    b)setnx+expire
    c)setnx+get+getset

  • 3、基于ZooKeeper实现
    zookeeper分布式锁是基于zk的临时有序节点的特性实现的,其主要思想为:每个客户端对某个功能加锁时,在zookeeper上的与该功能对应的指定节点的目录下,生成一个唯一的临时有序节点。判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题

总结

基本把我对分布式锁的了解大概讲了一遍。可以实现分布式锁功能的,包括数据库、nosql、分布式协调服务等等。根据业务的场景、现状以及已经依赖的服务,应用可以使用不同分布式锁实现。