Fork me on GitHub

分布式锁实现方式

分布式锁实现方式

1.基于数据库

创建一个专门的锁表,通过操作数据来实现。基于数据库的锁性能相对较差,一般都是首先排除的方案,必要才用。

  1. 基于唯一索引实现

    以方法名为唯一索引,每次使用时插入,结束时删除,当无法插入时表示有人占有锁。这种方案的问题是服务挂掉可能数据一直存在,导致锁一直没有删除释放,需要定时清理。

  2. 基于悲观锁实现

    通过悲观锁查询一条记录锁定,当其他线程查询时必须block等待,等待超时则失败(InnoDb默认等待时间50s,innodb_lock_wait_timeout设置事务锁超时时间)。悲观锁可以严格保证数据访问的安全。但是缺点也明显,即每次请求都会额外产生加锁的开销且未获取到锁的请求将会阻塞等待锁的获取,在高并发环境下,容易造成大量请求阻塞,影响系统可用性。另外,悲观锁使用不当还可能产生死锁的情况。

  3. 基于乐观锁实现

    设置version字段,控制版本,每次使用前查询,使用后根据version值更新version+1,如果更新影响行数为0说明资源已经被修改了。乐观锁适合并发量不高的情况,并发量高会大量更新,增加数据库压力。

2. 基于缓存

  1. 基于redis

    加锁实际上就是在redis中,给Key键设置一个值,为避免死锁,并给定一个过期时间。

    1
    2
    3
    4
    5
    SET lock_key random_value NX PX 5000
    值得注意的是:
    `random_value` 是客户端生成的唯一的字符串
    `NX` 代表只在键不存在时,才对键进行设置操作
    `PX 5000` 设置键的过期时间为5000毫秒

    这样,如果上面的命令执行成功,则证明客户端获取到了锁。删除key即释放锁。主要缺点是锁不具有重入性,主从集群时,主还未同步就挂掉,此时从会变为主,此时仍可以获取锁。

  2. 基于redisson

    内部是hash结构,通过keyName来判断锁存不存在,不存在设置值和过期时间加锁成功,如果已存在判断是否为当前线程,不是当前线程失败,是当前线程则计数加1(可重入实现)。加锁和解锁都是通过lua脚本代码来实现的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    //redisson github地址:https://github.com/redisson/redisson
    public static void main(String[] args) {
    Config config = new Config();
    config.useSingleServer().setAddress("redis://127.0.0.1:6379");
    config.useSingleServer().setPassword("redis1234");
    final RedissonClient client = Redisson.create(config);
    RLock lock = client.getLock("lock1");
    try{
    lock.lock();
    }finally{
    lock.unlock();
    }
    }

3. 基于zookeeper

创建临时有序节点,每个线程均能创建节点成功,但是其序号不同,只有序号最小的可以拥有锁,其它线程只需要监听比自己序号小的节点状态即可。可以通过curator框架来实现。

基本思路如下:

  1. 在你指定的节点下创建一个锁目录lock

  2. 线程X进来获取锁在lock目录下,并创建临时有序节点

  3. 线程X获取lock目录下所有子节点,并获取比自己小的兄弟节点,如果不存在比自己小的节点,说明当前线程序号最小,顺利获取锁

  4. 此时线程Y进来创建临时节点并获取兄弟节点 ,判断自己是否为最小序号节点,发现不是,于是设置监听(watch)比自己小的节点(这里是为了发生上面说的羊群效应)

  5. 线程X执行完逻辑,删除自己的节点,线程Y监听到节点有变化,进一步判断自己是已经是最小节点,顺利获取锁


-------------本文结束感谢您的阅读-------------
感谢你的支持!