分布式锁实现
在分布式系统中,分布式锁是解决并发控制和数据一致性问题的关键技术。它确保了在多进程或多线程环境下,对共享资源的访问是互斥的,从而避免了数据冲突和不一致性。本文将深入探讨分布式锁的概念、常见实现方式。
分布式锁概念
分布式锁是满足分布式系统或集群模式下多进程可见并且互斥的锁。其核心思想是让所有进程使用同一把锁,通过锁来控制线程的执行,使程序在并发环境下能够串行执行,从而保证数据的 安全性和一致性。
常见的分布式锁
常见的分布式锁有三种:Mysql、Redis 和 Zookeeper。
- Mysql:Mysql 本身带有锁机制,但由于其性能一般,在分布式锁的应用中相对较少。
- Redis:Redis 作为分布式锁是非常常见的一种方式,在企业级开发中广泛应用。它利用
setnx
命令来实现锁的获取,如果插入键成功,则表示获取到锁,否则表示获取锁失败。此外,还可以通过设置过期时间来自动释放锁,避免死锁的发生。 - Zookeeper:Zookeeper 也是企业级开发中实现分布式锁的较好方案,它提供了强大的分布式协调能力,能够确保锁的可靠性和一致性。
实现思路
- 获取锁:
- 互斥:确保在同一时刻只能有一个线程获取到锁,其他线程必须等待。
- 非阻塞:尝试获取锁一次,如果成功则返回
true
,否则返回false
,避免线程阻塞等待。
- 释放锁:
- 手动释放:线 程在完成任务后,应及时手动释放锁,以便其他线程能够获取锁。
- 超时释放:在获取锁时设置一个超时时间,如果线程在超时时间内未完成任务,锁将自动释放,避免死锁的发生。
为了确保锁的操作原子性,通常使用一些原子性操作或脚本,如 Redis 中的 Lua 脚本,来保证获取锁和设置过期时间的操作是一个原子操作,避免出现部分成功的情况。例如
setnx lock thread1
expire lock 5
有可能在执行完第一句话的时候,服务器挂了,过期时间就无法执行,造成死锁的情况,无法保证原子性,因此我们想要这两个操作同时执行,可以使用 下面这种方式,过期时间ex 为10s:
set lock thread ex 10 nx
分布式锁实现
锁的基本接口:
public interface ILock {
/**
* 尝试获取锁
* @param timeoutSec 超时时间,单位秒 过期自动释放锁
* @return true 获取成功,false 获取失败
*/
boolean tryLock(long timeoutSec);
/**
* 释放锁
*/
void unlock();
}
实现类:
public class SimpleRedisLock implements ILock {
private String name;
private StringRedisTemplate stringRedisTemplate;
public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}
public static final String KEY_PREFIX = "lock:";
@Override
public boolean tryLock(long timeoutSec) {
//获取当前线程的id
long threadId = Thread.currentThread().getId();
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(KEY_PREFIX + name, String.valueOf(threadId),
timeoutSec, TimeUnit.SECONDS);
return BooleanUtil.isTrue(success);
}
@Override
public void unlock() {
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}
修改业务代码: 原来有问题的代码:
Long userId = UserHolder.getUser().getId();
synchronized (userId.toString().intern()) {
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
}
修改如下 :
Long userId = UserHolder.getUser().getId();
SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
boolean isLock = lock.tryLock(5);
if (!isLock) {
//获取失败,返回错误或者 重试
return Result.fail("服务器繁忙");
}
try {
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
} finally {
lock.unlock();
}
##Redis 分布式锁误删问题
误删问题描述:持有锁的线程在锁的内部出现阻塞,导致锁自动释放,此时其他线程(如线程 2)尝试获取锁并成功获得。 然后线程 2 在持有锁执行过程中,线程 1 反应过来继续执行,当线程 1 走到删除锁逻辑时,会误将本应该属于线程 2 的锁删除,这就是误删别人锁的情况。
解决方案:在获取锁时存入线程标示(可以用 UUID 表示),在释放锁时先获取锁中的线程标示,判断是否与当前线程标示一致。如果一致,则释放锁;如果不一致,则不进行锁的删除。
具体实现代码如下:
public static final String KEY_PREFIX = "lock:";
public static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
@Override
public boolean tryLock(long timeoutSec) {
// 获取当前线程的id
String threadId = ID_PREFIX + Thread.currentThread().getId();
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(KEY_PREFIX + name, String.valueOf(threadId),
timeoutSec, TimeUnit.SECONDS);
return BooleanUtil.isTrue(success);
}
@Override
public void unlock() {
// 获取当前线程的id
String threadId = ID_PREFIX + Thread.currentThread().getId();
// 获取锁的值
String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
if (threadId.equals(id)) {
// 一致,说明是当前线程的锁,删除
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}
分布式锁原子性问题
问题描述:线程 1 持有锁后,在执行业务逻辑过程中准备删除锁,并且已经走到了条件判断的过程,即确认当前锁属于自己,正准备删除锁时,锁到期了。此时线程 2 进入,而线程 1 在卡顿结束后会继续执行删除锁的代码,导致条件判断没有起到作用,这就是删锁时的原子性问题。出现这个问题的原因是线程 1 的拿锁、判断锁、删锁操作并不是原子性的。
解决方案:使用 Lua 脚本解决多条命令的原子性问题。Lua 脚本可以编写多条 Redis 命令,确保这些命令执行时的原子性。
Lua脚本解决多条命令原子性问题
lua脚本可以编写多条redis命令,确保多条命令执行时的原子性. 调用函数
redis.call("命令名称 ","key","其他参数 ")
例如:
-- 执行set name jack
redis.call("set","name","jack")
先执行set name Rose,再执行get name,则脚本如下:
--先执行set name Rose,再执行get name,则脚本如下:
redis.call("set","name","Rose")
local name = redis.call("get", "name")
return name
例如,释放锁的业务流程如下:
- 获取锁中的线程标示。
- 判断是否与指定的标示(当前线程标示)一致。
- 如果一致则释放锁(删除)。
- 如果不一致则什么都不做。
对应的Lua脚本为:
if (redis.call('GET', KEYS[1]) == ARGV[1]) then
-- 一致,则删除锁
return redis.call('DEL', KEYS[1])
end
-- 不一致,则直接返回
return 0
修改代码:
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
static {
UNLOCK_SCRIPT = new DefaultRedisScript<>();
UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
UNLOCK_SCRIPT.setResultType(Long.class);
}
@Override
public void unlock() {
//调用lua脚本
stringRedisTemplate.execute(
UNLOCK_SCRIPT,
Collections.singletonList(KEY_PREFIX + name),
ID_PREFIX + Thread.currentThread().getId());
}