为什么要设计分布式锁
在简单的单机系统中,当存在多个线程同时要修改某个共享变量时,为了数据的操作安全,往往需要通过加锁的方法,在同一时刻同一代码块只能有一个进程执行操作,存在很多加锁的方式,比如在java中有synchronize或Lock子类等。 但是在分布式中,会存在多个主机,即会存在多个jvm, 在jvm之间数据是不能共享的,上面的方法只能在一个jvm中执行有效,在多个jvm中同一变量可能会有不同的值。所以我们要设计一种跨jvm的共享互斥机制来控制共享变量资源的访问,这也是提出分布式锁的初衷。
需要解决的问题
为了将分布式锁实现较好的性能,我们需要解决下面几个重要的问题:
- 一个方法或代码片段在同一时刻只能被一个进程所执行。
- 高可用的获取锁与释放锁功能。
- 避免死锁
- 锁只能被持有该锁的客户端删除或者释放。
- 容错,在服务器宕机时,锁依然能得到释放或者其他服务器可以进行加锁。
下面分别利用redis和zookeeper来实现加锁和解锁机制。
基于Redis的加锁第一版
本版本通过变量sign设置锁的唯一标识,确保只有拥有该锁的客户端才能删除它,其他客户端不能删除。利用阻塞锁的思想, 通过while(System.currentTimeMillis() < endTime)和Thread.sleep()相结合,在设置的规定时间内进行多次尝试。但是setnx操作和expire分割开了,不具有原子性,可能会出现问题。比如说,在执行到jedis.expire时,可能系统发生了崩溃,导致锁没有设置过期时间,导致发生死锁。
public String addLockVersion1(String key, int blockTime, int expireTime) { if (blockTime <=0 || expireTime <= 0) return null; Jedis jedis = null; try { jedis = jedisPool.getResource(); String sign = UUID.randomUUID().toString(); String token = null; //设置阻塞尝试时间 long endTime = System.currentTimeMillis() + blockTime; while (System.currentTimeMillis()
基于Redis的加锁第二版
通过设置key对应的value值为锁的过期时间,当遇到系统崩溃,致使利用expire设置锁过期时间失败时,通过获取value值,来判断当前锁是否过期,如果该锁已经过期了,则进行重新获取。
但是它也存在一些问题。当锁过期时,如果多个进程同时执行jedis.getSet方法,虽然只有一个进程可以获得该锁,但是这个进程的锁的过期时间可能被其他进程的锁所覆盖。该锁没有设置唯一标识,也会被其他客户端锁释放,不满足只能被锁的拥有者锁释放的条件。
public boolean addLockVersion2(String key, int blockTime, int expireTime) { if (blockTime <=0 || expireTime <= 0) return false; Jedis jedis = null; try { jedis = jedisPool.getResource();