1. 分布式锁介绍
1.1. 概念
分布式锁是一种在分布式系统中实现互斥访问共享资源的机制。它用于控制多个节点对共享资源的并发访问,以确保数据的一致性和完整性。
1.2. 用途
- 保证数据一致性:在分布式系统中,多个节点可能会同时访问和修改共享资源,如果没有有效的控制机制,可能会导致数据不一致的问题。分布式锁可以确保在任何时刻只有一个节点可以修改共享资源,从而保证数据的一致性。
- 避免竞态条件:竞态条件是指多个节点同时对共享资源进行读写操作,导致数据混乱或冲突的情况。分布式锁可以避免竞态条件的发生,确保在任何时刻只有一个节点可以访问共享资源。
- 提高系统性能:通过控制对共享资源的并发访问,分布式锁可以减少不必要的等待和冲突,提高系统的整体性能。
1.3. 分布式锁与传统线程锁的区别
与传统的锁相比,分布式锁的主要区别在于其分布性和异构性。传统的锁通常是在同一进程或线程之间实现互斥访问的,而分布式锁则需要考虑不同节点之间的通信和协作。此外,分布式锁可以应用于异构系统,即不同的节点可以采用不同的操作系统和编程语言,这使得分布式锁的实现更加复杂。
1.4. 使用分布式锁的原因
在分布式系统中使用锁的原因主要是因为多个节点之间存在数据共享和并发访问的问题。如果没有有效的控制机制,可能会导致数据不一致、竞态条件等问题的发生。通过使用分布式锁,可以确保对共享资源的互斥访问,从而保证数据的一致性和完整性。
2. 分布式锁的实现方式
- 基于数据库实现分布式锁:利用数据库的唯一索引或行锁等机制来实现分布式锁。通过在数据库中插入锁记录或特定字段,可以控制对共享资源的访问。这种方法需要数据库支持相应的锁机制,并保证数据库的高可用性。
- 基于缓存系统实现分布式锁:利用缓存系统的原子操作和一致性哈希算法等机制来实现分布式锁。常见的缓存系统有Redis、Memcached等。通过在缓存系统中设置特定键或使用分布式锁协议,可以控制对共享资源的访问。这种方法具有高性能和简单易用的优点,但需要保证缓存系统的可用性和可靠性。
- 基于ZooKeeper实现分布式锁:利用ZooKeeper的原子操作和有序节点等机制来实现分布式锁。ZooKeeper可以作为配置中心,协调和管理分布式系统中的节点。通过在ZooKeeper中创建临时有序节点或使用ZooKeeper的分布式锁服务,可以控制对共享资源的访问。这种方法具有高可用性和可扩展性,但需要保证ZooKeeper服务的可用性和可靠性。
- 基于分布式消息队列实现分布式锁:利用消息队列的发布订阅模式和事务保证等机制来实现分布式锁。常见的消息队列系统有RabbitMQ、Kafka等。通过在消息队列中发送特定消息或使用消息队列的事务保证机制,可以控制对共享资源的访问。这种方法具有解耦和异步处理的优点,但需要保证消息队列系统的可用性和可靠性。
3. redis实现分布式锁的方案
3.1. 使用redis命令加锁
- SETNX + DELETE:通过SETNX(SET if Not eXists)命令尝试设置一个键,如果键不存在则设置成功并返回1,否则返回0。在任务执行结束后使用DELETE命令删除键。
缺点:当程序在执行任务时宕机了,会造成key无法删除造成系统死锁。
Boolean lockSuccess = redisTemplate.opsForValue().setIfAbsent("lock","lockValue");
if(lockSuccess){
//加锁成功。。。执行业务
redisTemplate.delete("lock");//执行完毕,删除锁
}
- SETNX + EXPIRE:通过SETNX(SET if Not eXists)命令尝试设置一个键,如果键不存在则设置成功并返回1,否则返回0。然后使用EXPIRE命令为该键设置一个过期时间,防止锁未及时释放而导致的死锁问题,在任务执行结束后使用DELETE命令删除键。
缺点:这种方法实现简单,但存在一个问题,即如果SETNX成功后客户端在执行EXPIRE之前崩溃,键将一直存在,造成死锁。
Boolean lockSuccess = redisTemplate.opsForValue().setIfAbsent("lock","lockValue");
if(lockSuccess){
//设置过期时间
redisTemplate.expire("lock",30,TimeUnit.SECONDS);
//加锁成功。。。执行业务
redisTemplate.delete("lock");//执行完毕,删除锁
}
- SET KEY VALUE EX PX NX + DELETE:通过SET命令的EX(过期时间)和PX(精确的过期时间)选项来设置键的过期时间,NX(Only if Not eXists)选项来确保只有在键不存在时才设置成功,在任务执行结束后使用DELETE命令删除键。
缺点:此方法当中的任务执行时间过长时,可能会在任务还未结束的时候key就过期了,其他进程就能够获取到锁,造成任务的异常
Boolean lockSuccess = redisTemplate.opsForValue().setIfAbsent("lock","lockValue",300,TimeUnit.SECONDS);
if(lockSuccess){
//加锁成功。。。执行业务
//当任务时间过长时,可能任务还未结束,锁还未释放就会被其他进程获取
redisTemplate.delete("lock");//执行完毕,删除锁
}
- 使用lua脚本实现分布式锁:利用lua脚本,我们可以增加一些复杂的加锁、解锁逻辑,实现更复杂的应用场景。lua脚本是一条命令发送到redis服务器执行的,且lua执行期间无法执行其他命令,因此lua可以做到原子加锁、原子解锁
缺点:当任务执行时间过长时,锁到期了无法续期,如果实现续期还需要自己再编写实现
//-- lua加锁脚本
//-- KEYS[1],ARGV[1],ARGV[2]分别对应了锁的键,锁标识,锁过期时间
//-- 如果setnx成功,则继续expire命令逻辑
String lockScript = "if redis.call('setnx',KEYS[1],ARGV[1]) == 1 \n" +
" then \n" +
" -- 则给同一个key设置过期时间\n" +
" redis.call('expire',KEYS[1],ARGV[2]) \n" +
" return 1 \n" +
" else \n" +
" -- 如果setnx失败,则返回0\n" +
" return 0 \n" +
"end";
//
Integer lockResult = redisTemplate.execute(new RedisCallback<Object>() {
@Override
public Object doInRedis(RedisConnection connection) throws DataAccessException {
byte[][] keysAndArgs = new byte[][]{"lock_key".getBytes(), "uuid".getBytes(), "3000".getBytes()};
return connection.eval(lockScript.getBytes(), ReturnType.INTEGER, 1, keysAndArgs);
}
});
if((lockResult === 1)) {
//加锁成功,执行业务逻辑
//-- lua解锁脚本
//-- KEYS[1],ARGV[1]分别对应了锁的键,锁标识
//-- 若无法获取锁的键缓存,则认为已经解锁
String deleteScript = "if redis.call('get',KEYS[1]) == false \n" +
" then \n" +
" return 1 \n" +
" -- 若获取到orderId,并value值对应了uuid,则执行删除命令\n" +
" elseif redis.call('get',KEYS[1]) == ARGV[1] \n" +
" then \n" +
" -- 删除缓存中的key\n" +
" \treturn redis.call('del',KEYS[1]) \n" +
" else \n" +
" -- 若获取到orderId,且value值与存入时不一致,则返回特殊值,方便进行后续逻辑\n" +
" return 2 \n" +
"end";
redisTemplate.execute(new RedisCallback<Object>() {
@Override
public Object doInRedis(RedisConnection connection) throws DataAccessException {
byte[][] keysAndArgs = new byte[][]{"lock_key".getBytes(), "uuid".getBytes()};
return connection.eval(deleteScript.getBytes(), ReturnType.INTEGER, 1, keysAndArgs);
}
});
}
- 使用redisson分布式锁——专业的分布式锁解决方案
redisson是一个专业的基于redis实现的分布式锁组件,其提供了比较全面的分布式锁解决方案,包括看门狗机制实现锁自动续期、防锁死、非阻塞获取锁等。
redisson的看门狗机制可以自动为我们的分布式锁续期时间,当任务未结束但是锁快要到期时,看门狗线程会自动为我们的锁延长时间,其实现也用到了lua脚本,感兴趣的读者可以深入研究。
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
public class RedissonLockExample {
public static void main(String[] args) throws InterruptedException {
// 1. 创建配置
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
// 2. 创建 Redisson 客户端
RedissonClient redissonClient = Redisson.create(config);
// 3. 获取锁对象
RLock lock = redissonClient.getLock("myLock");
// 4. 加锁
lock.lock();
try {
// 5. 执行任务代码
// ...
} finally {
// 6. 解锁
lock.unlock();
}
// 7. 关闭 Redisson 客户端连接
redissonClient.shutdown();
}
}
4. 总结
分布式锁的实现方案有很多,其目的就是为了在分布式环境中提供一种可让多个进程顺序操作共享资源。本文主要介绍了使用redis实现分布式锁的几种方案,读者可根据实际场景灵活选择。
分布式锁的大部分实现方式都是通过设置一个分布式标志位来实现的,其思想大致相同,如有其他可提供分布式操作的中间件,我们也可以进行基于其中间件的拓展。