分布式锁
满足分布式系统或集群模式下多进程可见并且互斥的锁
常见的分布式锁实现
- MySQL
- Redis
- Zookeeper
- etcd
…
比较
锁类型 |
互斥 |
高可用 |
高性能 |
安全性 |
MySQL |
本身的互斥锁机制 |
好 |
一般 |
断开连接,自动释放锁 |
Redis |
通过 SETNX 实现 |
好 |
好 |
过期时间,自动释放锁 |
Zookeeper |
节点唯一性与有序性实现互斥 |
好 |
一般 |
临时节点,断开连接,自动释放锁 |
Redis 分布式锁
互斥:确保只能有一个线程获取锁
非阻塞:尝试一次,成功返回true,失败则返回false
1 2 3 4 5
| SETNX lock_key lock_value
EXPIRE lock_key 10
|
p.s. 如果添加锁后服务器立刻宕机,没有成功设置过期时间,锁会一直存在,导致死锁
因此需要保证添加锁和设置过期时间是原子操作,可以使用 Redis 的 SET 命令来实现原子操作
1 2
| SET lock_key lock_value NX EX 10
|
实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public interface ILock {
boolean tryLock(long timeoutSec);
void unLock(); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.concurrent.TimeUnit;
public class SimpleRedisLock implements ILock {
private String name; private StringRedisTemplate stringRedisTemplate;
public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) { this.name = name; this.stringRedisTemplate = stringRedisTemplate; }
private static final String KEY_PREFIX = "lock:";
@Override public boolean tryLock(long timeoutSec) { long threadId = Thread.currentThread().getId(); boolean success = stringRedisTemplate.opsForValue() .setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS); return Boolean.TRUE.equals(success); }
@Override public void unLock() { stringRedisTemplate.delete(KEY_PREFIX + name); } }
|
修改原来的一人一单代码,改为使用分布式锁
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| @Override public Result seckillVoucher(Long voucherId) { ...
SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate); boolean isLock = lock.tryLock(1200); if (!isLock) { return Result.fail("不允许重复下单"); } try { IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy(); return proxy.createVoucherOrder(voucherId); } finally { lock.unLock(); }
}
|
新的问题
如果一个线程获取锁后,执行了很长时间,导致锁过期了,那么其他线程就会获取到锁,导致误删其他锁,引发数据不一致,这就是业务堵塞导致锁过期的问题。
解决方案:
在获取锁时存入线程标识(可以用UUID),因为不同的jvm可能会存在相同的线程标识,需要进一步区分,以保证全局唯一性
在释放锁时,判断当前线程标识是否和锁中的线程标识一致,如果一致则释放锁,否则不释放锁
实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| import cn.hutool.core.lang.UUID; import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.concurrent.TimeUnit;
public class SimpleRedisLock implements ILock {
private String name; private StringRedisTemplate stringRedisTemplate;
public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) { this.name = name; this.stringRedisTemplate = stringRedisTemplate; }
private static final String KEY_PREFIX = "lock:"; private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
@Override public boolean tryLock(long timeoutSec) { String threadId = ID_PREFIX + Thread.currentThread().getId(); boolean success = stringRedisTemplate.opsForValue() .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS); return Boolean.TRUE.equals(success); }
@Override public void unLock() { String threadId = ID_PREFIX + Thread.currentThread().getId(); String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name); if (threadId.equals(id)) { stringRedisTemplate.delete(KEY_PREFIX + name); } } }
|
这样可以避免因为业务堵塞导致锁过期而引发的误删锁问题
新的问题
如果一个线程获取锁后,已经进行完释放锁前的判断标识操作,此时如果因为gc阻塞了,导致锁过期了,那么其他线程就会获取到锁,导致释放了其他线程的锁,引发数据不一致,这就是标识判断与释放锁不是原子操作的问题。
注意,因为已经进行了判断标识操作,所以之后会直接释放其他线程的锁

解决方案:
使用Lua脚本来实现原子操作
Redis Lua脚本
Redis 提供了 Lua 脚本的支持,在一个脚本中编写多条 Redis 命令,确保多条命令执行时的原子性。