在Java分布式项目中,分布式锁用于确保在分布式系统环境下,对共享资源的访问能够同步进行,防止数据不一致的问题。常见的分布式锁实现方式主要有基于数据库、基于缓存(如Redis)、基于ZooKeeper等。
1. 基于数据库的分布式锁
基于数据库实现分布式锁主要是通过数据库的唯一索引或行锁特性来保证锁的唯一性。
- 使用唯一索引:在数据库中创建一个锁表,表中有一个唯一索引字段。当某个服务实例需要获取锁时,它尝试在该表中插入一条具有唯一索引值的记录。如果插入成功,表示获取锁成功;如果因为唯一索引冲突而失败,则表示锁已被其他实例获取。
- 使用行锁:选定一个表的某一行作为锁标识,通过对这一行进行更新操作来尝试获取锁(比如更新一个时间戳字段),利用数据库的行锁机制来实现分布式锁。
2. 基于缓存的分布式锁(以Redis为例)
Redis分布式锁的核心思想是利用Redis的原子命令来创建一个锁。最常用的方法是通过SET
命令加上某些选项,如NX
(只在键不存在时设置键)、PX
(键的过期时间,以毫秒为单位)来实现。
基于RedLock算法的实现
在Java中,我们可以使用Redisson客户端库来实现RedLock算法,Redisson已经提供了对RedLock的封装,使得在Java中实现RedLock变得相对简单。
首先,确保你的项目中加入了Redisson依赖。如果是使用Maven,可以在pom.xml
中添加以下依赖:
<dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>最新版本</version>
</dependency>
接下来,是一个使用Redisson实现RedLock的简单示例:
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;import java.util.concurrent.TimeUnit;public class RedLockExample {public static void main(String[] args) {// 配置每个Redis实例Config config1 = new Config();config1.useSingleServer().setAddress("redis://127.0.0.1:6379");RedissonClient redissonClient1 = Redisson.create(config1);Config config2 = new Config();config2.useSingleServer().setAddress("redis://127.0.0.2:6379");RedissonClient redissonClient2 = Redisson.create(config2);Config config3 = new Config();config3.useSingleServer().setAddress("redis://127.0.0.3:6379");RedissonClient redissonClient3 = Redisson.create(config3);// 获取三个Redis实例上的锁对象RLock lock1 = redissonClient1.getLock("myLock");RLock lock2 = redissonClient2.getLock("myLock");RLock lock3 = redissonClient3.getLock("myLock");// 创建RedLock(红锁)RLock redLock = redissonClient1.getRedLock(lock1, lock2, lock3);try {// 尝试获取锁,最多等待100秒,锁定后最多持有锁60秒boolean isLocked = redLock.tryLock(100, 60, TimeUnit.SECONDS);if (isLocked) {// 如果获取到锁,执行业务逻辑try {System.out.println("Lock acquired, executing some task");} finally {// 无论如何,最后都要释放锁redLock.unlock();System.out.println("Lock released");}}} catch (InterruptedException e) {e.printStackTrace();} finally {// 关闭Redisson客户端redissonClient1.shutdown();redissonClient2.shutdown();redissonClient3.shutdown();}}
}
这个例子中,我们首先创建了三个指向不同Redis实例的Redisson客户端。然后,我们从每个客户端获取了一个锁对象,并使用这些锁对象创建了一个RedLock。接着,我们尝试获取这个RedLock,如果成功,就执行一些业务逻辑,最后释放锁并关闭客户端。
在Redisson的RedLock实现中,当你尝试获取锁时,底层机制涉及到了键(key)和值(value)的设置,但这些都被Redisson内部封装了起来,因此在使用Redisson时你不需要直接操作这些底层细节。
获取锁的工作原理:
当调用redLock.tryLock()
方法时,Redisson会对每个参与的Redis实例执行以下操作:
-
生成一个唯一的锁标识(UUID):这个UUID是作为value存储在Redis中的,每次尝试获取锁时都会生成一个新的UUID。这保证了锁的公平性和唯一性。
-
尝试在所有配置的Redis实例上设置锁:Redisson会向每个Redis实例发送一个带有NX(只在键不存在时设置键)和PX(键的过期时间,单位是毫秒)选项的SET命令,其中键是你指定的锁名(如"myLock"),值是第一步中生成的UUID。这个操作是基于Redis的SET命令实现的,确保了操作的原子性。
-
计算成功获取锁的实例数量:如果成功在大多数Redis实例上设置了键,则认为成功获取了锁。具体来说,假设参与的Redis实例总数为N,那么至少需要在N/2+1个实例上成功设置了键,锁才算获取成功。
-
检查锁获取时间:为了确保锁的有效性,Redisson还会检查获取锁所花费的时间。如果获取锁的时间超过了从第一个Redis实例开始尝试到最后一个Redis实例尝试的总时间,则认为获取锁失败,此时会自动释放在前面步骤中成功设置的所有Redis实例上的锁。
释放锁:
当调用redLock.unlock()
方法时,Redisson会在每个Redis实例上执行一段Lua脚本。这段脚本会检查给定键的值是否与尝试获取锁时生成的UUID相匹配。如果匹配,则删除该键,释放锁。这个过程确保了只有锁的持有者能够释放锁。
封装细节:
由于Redisson已经封装了这些操作,因此使用Redisson时,你不需要直接设置key和value。你只需要通过getLock
方法指定锁名,并通过tryLock
和unlock
方法来获取和释放锁。这使得使用Redisson实现分布式锁变得非常简单和直接,同时也隐藏了实现的复杂性。
3. 基于ZooKeeper的分布式锁
ZooKeeper实现分布式锁的思路是利用其节点(ZNode)的唯一性和临时性(临时节点在客户端断开连接时自动删除)特性。
举例说明:
- 创建一个代表锁的持久节点,比如
/mylock
。 - 当一个客户端尝试获得锁时,它在
/mylock
下创建一个临时顺序节点,如/mylock/lock_00000001
。 - 客户端获取
/mylock
下所有子节点,并比较自己创建的节点序号。如果该节点序号最小,那么认为该客户端获得了锁。 - 当持有锁的客户端完成其任务后,它会删除自己创建的那个临时顺序节点。同时,剩下的等待锁的客户端会收到ZooKeeper的通知,重新执行第3步的逻辑,以此类推。
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;public class ZooKeeperLock {private ZooKeeper zooKeeper;public ZooKeeperLock(ZooKeeper zooKeeper) {this.zooKeeper = zooKeeper;}// 方法实现...
}