一、面试聊聊-分布式锁,如何回答?
要分析分布式锁这个问题,我们根据黄金圈法则来分析。
黄金圈法则是由美国营销顾问西蒙·斯涅克(Simon Sinek)提出的一种思维模型,用于帮助人们更好地理解和传达信息。黄金圈法则由三个圈组成,分别是:
- 为什么(Why):为什么要做这件事?这是黄金圈的核心,是一切的起点。
- 怎么做(How):怎么做这件事?这是黄金圈的中间部分,是实现目标的方法。
- 做什么(What):做什么这件事?这是黄金圈的外围部分,是具体的行为。
使用3w分析问题思路来分析分布式锁,可以从以下几个方面进行分析:
What:分布式锁是什么?
分布式锁是控制分布式系统之间同步访问共享资源的机制。在分布式系统中,常常需要协调他们的动作。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,在这种情况下,便需要使用到分布式锁。
回答时:先说一下概念。分布式锁是用于在分布式系统中控制对共享资源的访问,以避免数据竞争和并发问题。
How:如何实现分布式锁?
分布式锁的实现方法有很多,常见的有以下几种:
- 数据库锁:使用数据库中的行锁或表锁来实现分布式锁。
- 文件锁:使用文件来实现分布式锁。
- Zookeeper锁:使用Zookeeper来实现分布式锁。
- Redis锁:使用Redis来实现分布式锁。
- 消息队列锁:使用消息队列来实现分布式锁。
回答时:说一下分布式锁以上的实现方式。
Why:为什么需要分布式锁?
一些需要分布式锁的场景:
- 分布式数据库事务:在分布式系统中,通常需要使用分布式事务来保证数据的一致性。在分布式事务中,通常会使用分布式锁来保证事务的执行顺序。
- 分布式资源分配:在分布式系统中,通常需要使用分布式锁来分配共享资源。例如,在抢购场景中,需要使用分布式锁来保证同一商品只能被一个用户购买。
- 分布式数据同步:在分布式系统中,通常需要使用分布式锁来保证数据的同步。例如,在订单系统中,需要使用分布式锁来保证同一订单只能被一个系统修改。
回答时:说一下分布式锁的应用场景。
二、深入聊聊-分布式锁在项目中的应用
1、Redisson实现分布式锁
Redisson 是一个基于 Redis 的 Java 分布式框架。Redisson 提供了丰富的功能,包括分布式锁、分布式集合、分布式队列等。
以下是使用 Redisson 实现分布式锁的示例:
@Autowired
private RedissonClient redissonClient;
public String lock() {
// 获取锁
RLock lock = redissonClient.getLock("lock");
boolean acquired = lock.tryLock(10,-1,TimeUnit.SECONDS);
if (acquired) {
// 获取锁成功,执行业务逻辑
return "获取锁成功,执行业务逻辑...";
} else {
// 获取锁失败,重试
return "获取锁失败,重试...";
}
}
public String unlock() {
// 释放锁
RLock lock = redissonClient.getLock("lock");
lock.unlock();
return "释放锁成功...";
}
另外,redisson支持锁续期。即在锁键值过期后任务还没执行完成,此时需要把锁键值的时间自动延长。
Redisson提供了的续期机制,只要客户端加锁成功,就会启动一个Watch Dog。可以看到源代码的实现leaseTime不设置为-1时开启监听。如果任务没完成就调用scheduleExpirationRenewal续期方法。
tryLock() 方法用于尝试获取分布式锁,该方法有三个参数:
- key:锁的键值。
- waitTime:等待获取锁的时间,单位为毫秒。
- leaseTime:锁的过期时间,单位为毫秒。
waitTime 参数表示客户端最多等待多长时间来获取锁。如果在 waitTime 时间内没有获取到锁,则会返回 false。
leaseTime 参数表示锁的过期时间。如果锁在 leaseTime 时间内没有被释放,则会自动释放。如果 leaseTime 设置为 -1,则表示锁的过期时间由 renew() 方法来控制。这样,在业务逻辑执行过程中,可以定期调用 lock.renew() 方法来续期锁的过期时间。
tryLock() 方法的返回值是一个 Boolean 值,表示是否成功获取到锁。如果成功获取到锁,则返回 true。否则,返回 false。
2、Springboot+Lettuce
在 SpringBoot 2.7 中,可以通过spring-boot-starter-data-redis 默认依赖是Lettuce。那么Lettuce是如何实现分布式锁呢
- 在 Spring Boot 项目中添加 spring-boot-starter-data-redis 依赖。
- 定义一个 RedisLock 类来封装分布式锁的相关操作。
- 在 ServiceImpl类中使用 RedisLock 类来获取和释放分布式锁。
以下是 SpringBoot+Lettuce 实现分布式锁的完整代码:
@Component
public class RedisLock {
private static final String LOCK_SCRIPT = "if redis.call('exists', KEYS[1]) == 0 then\n" +
" redis.call('set', KEYS[1], ARGV[1], 'NX', 'PX', ARGV[2]);\n" +
" return 1\n" +
"else\n" +
" return 0\n" +
"end";
private final RedisTemplate<String, String> redisTemplate;
public RedisLock(RedisTemplate<String, String> redisTemplate) {
this.redisTemplate = redisTemplate;
}
//获得锁
public boolean acquireLock(String key, long timeout) {
String uuid = UUID.randomUUID().toString();
Object result = redisTemplate.execute(new DefaultRedisScript(LOCK_SCRIPT, Long.class), Arrays.asList(key), uuid, timeout);
return result != null && (long) result == 1;
}
//释放锁
public void releaseLock(String key, String uuid) {
redisTemplate.delete(key);
}
}
@Service
public class TestServcieImpl implements TestServcie{
@Autowired
private StringRedisTemplate stringRedisTemplate;
// 加锁脚本
private static final String LOCK_SCRIPT = "if redis.call ('setnx', KEYS[1], ARGV[1]) == 1 then return redis.call ('expire', KEYS[1], ARGV[2]) else return 0 end";
// 解锁脚本
private static final String UNLOCK_SCRIPT = "if redis.call ('get', KEYS[1]) == ARGV[1] then return redis.call ('del', KEYS[1]) else return 0 end";
// 加锁方法
public boolean lock(String key, String value, Long expire) {
RedisScript<Long> redisScript = new DefaultRedisScript<>(LOCK_SCRIPT, Long.class);
Long result = stringRedisTemplate.execute(redisScript, Collections.singletonList(key), value, String.valueOf(expire));
return result.equals(Long.valueOf(1));
}
// 解锁方法
public boolean unlock(String key, String value) {
RedisScript<Long> redisScript = new DefaultRedisScript<>(UNLOCK_SCRIPT, Long.class);
Long result = stringRedisTemplate.execute(redisScript, Collections.singletonList(key), value);
return result.equals(Long.valueOf(1));
}
}
在上述 demo 中,我们使用 RedisLock 类来封装分布式锁的相关操作。acquireLock() 方法用于获取分布式锁,releaseLock() 方法用于释放分布式锁。
在 ServcieImpl 类中,我们使用 RedisLock 类来获取和释放分布式锁。lock() 方法用于获取锁,unlock() 方法用于释放锁。不像Redisson封装好了相应的方法,Lettuuce如果要实现锁续期就需要自己写监听器及相应的lua脚本。
三、小结
1、什么是Lua?
可以看到不论是Redisson还是Lettuce实现分布式锁都使用的Lua脚本,那我们先来了解一下什么是Lua脚本语言。
Lua 是一个小巧的脚本语言,由巴西里约热内卢天主教大学(Pontifical Catholic University of Rio de Janeiro)里的一个研究小组于 1993 年开发的。Lua 使用标准 C 语言编写并以源代码形式开放,几乎在所有操作系统和平台上都能编译运行。Lua 脚本可以调用 C/C++ 的函数,也可以被 C/C++ 代码调用,所以 Lua 在应用程序中可以被广泛应用。
Lua 的特点如下:
- 小巧灵活:Lua 的核心只有 200K,非常小巧,可以方便地嵌入到应用程序中。Lua 的语法也非常简单,易于学习和使用。
- 可扩展性:Lua 可以调用 C/C++ 的函数,也可以被 C/C++ 代码调用,所以 Lua 可以很容易地扩展到应用程序的其他部分。
- 高性能:Lua 的运行效率非常高,可以满足大多数应用程序的需求。
Lua 在游戏开发、Web 开发、嵌入式系统等领域都有广泛的应用。
以下是 Lua 的一些典型应用:
- 游戏开发:Lua 常用于游戏中的脚本编写,用于实现游戏的逻辑和特效。
- Web 开发:Lua 可以用于 Web 开发,用于实现动态页面和游戏。
- 嵌入式系统:Lua 可以用于嵌入式系统的开发,用于实现控制逻辑和用户界面。
Lua 是一款非常实用的脚本语言,在众多领域都有广泛的应用。
2、为什么使用Lua?
- Redis 实现分布式锁中,获取锁、释放锁为什么要使用 Lua 脚本?
- 使用 Lua 脚本的主要原因是为了保证操作的原子性,避免出现并发问题或误解锁的情况。
- 使用 setnx 命令获取锁,然后使用 expire 命令设置过期时间,这两个命令之间可能会发生网络延迟或者其他异常,导致锁没有正确设置过期时间,从而造成死锁。
- 使用 del 命令释放锁,需要先判断锁是否属于当前客户端,否则可能会误解其他客户端的锁。
使用 Lua 脚本可以将判断和删除锁的操作合并为一个原子操作,避免了这些问题。Lua 脚本在 Redis 服务器端执行,不会受到网络延迟或者客户端故障的影响,也不会被其他命令打断,因此可以保证操作的原子性。
- 为什么说 Redis 命令没有原子性?
- Redis 命令本身是单线程执行的,所以单个命令是具有原子性的。
- 但是如果要实现分布式锁的功能,通常需要多个命令组合起来执行,例如 setnx + expire 或者 get + del。
- 这些命令组合在执行过程中可能会被其他客户端发送的命令打断,导致数据不一致或者逻辑错误。
因此,Redis 命令没有原子性是指多个命令组合起来执行时没有原子性。
以下是使用 Lua 脚本实现分布式锁的示例:
-- 获取锁
function acquire_lock(key, uuid, timeout)
local value = redis.call("GET", key)
if value == nil then
redis.call("SET", key, uuid, "NX", "PX", timeout)
return 1
else
return 0
end
end
-- 释放锁
function release_lock(key, uuid)
redis.call("DEL", key)
end
上述脚本实现了简单的 SETNX 和 DEL 操作,可以保证同一时刻只有一个客户端可以获取到锁。
在实际使用中,可以根据具体的业务场景来调整 Lua 脚本的实现。