这里是在完成分布式锁之前我们要先思考怎么才能实现在分布式环境下有着唯一的 id 锁标识。
其要满足的以下的特点:
我们常见并且能容易想到的生成策略有:
Redis自增id策略如下:
为了提高系统的安全性,我们一般不选择Redis的自增,以防被直接猜测出id,从而造成信息的泄露。
可以采用如下策略:用一个时间戳加上UUID来拼接成一个id
其中代码生成部分如下:
@Component
public class RedisIdWorker {
/**
* 开始时间戳、这里的是2022,6,12,0,0,0
*/
private static final long BEGIN_TIMESTAMP = 1654992000L;
/**
* 序列号位移位数
*/
private static final int COUNT_BITS = 32;
@Autowired
StringRedisTemplate stringRedisTemplate;
public long nextId(String keyPrefix){
//1. 生成时间戳
LocalDateTime now = LocalDateTime.now();
long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
long timestamp = nowSecond - BEGIN_TIMESTAMP;
//2. 生成序列号
//2.1 获取当前日期,精确到天
String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
Long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);
//3. 拼接并返回
//日期位数左移32位后填充序列号
return timestamp << COUNT_BITS | count;
}
}
其中Redis中如下:
也可以根据天数查询当前的具体订单数量
分布式锁是需要满足在分布式系统和集群系统下多线程可见并且互斥的锁,其有如下特点:
其中分布式锁的核心是实现多线程之间互斥,而满足这一点的方式。常见的有如下三种:
MySQL | Redis | Zookeeper | |
---|---|---|---|
互斥 | 利用mysql本身的互斥锁机制 | 利用setnx这样的互斥命令 | 利用节点的唯一性和有序性实现互斥 |
高可用 | 好 | 好 | 好 |
高性能 | 一般 | 好 | 一般 |
安全性 | 断开连接,自动释放锁 | 利用锁超时时间,到期释放 | 临时节点,断开连接自动释放 |
获取锁:
互斥:确保一个进程获取锁
setnx lock thread
expire lock 10
这里但是用这两了命令并不具有原子性,仍然可能在设锁后还没设置过期时间时宕机,因此可以采用set lock thread nx 10 ex来保证操作的原子性是引用
释放锁
在业务完成后手动释放
del key
超时释放:在获取锁的时候设置一个超时时间
如图,假如线程获取锁的同时的时候业务堵塞,锁由于超时被释放后、线程2获取到了锁开始执行业务的同时,线程1的堵塞消失,释放锁。由于此时是线程2的锁,导致线程2在执行的同时,线程3获取到了锁开始执行业务。那么锁的意义就不存在了,仍然会有线程安全问题。
如果想着增加过期时间来解决,其实只能降低这种问题出现的概率,并不能从根本上解决该问题。原因在于客户端在拿到锁之后,在操作共享资源时,遇到的场景是很复杂的,既然是预估的时间,也只能是大致的计算,不可能覆盖所有导致耗时变长的场景。
由于以上的超时释放导致的误删问题,我们想出对应的解决方案:
由于业务一般采用分布式集群的操作,可能造成线程的 id 相同,因此采用UUID或者时间戳来保证对应id的一致性(即上述的全局id生成器)
代码如下:
public class SimpleRedisLock implements ILock{
private String name;
private StringRedisTemplate stringRedisTemplate;
private static final String KEY_PREFIX = "lock:";
//true是去除uuid的横线
private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}
@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);
}
}
}
-- 获取线程标识
local id = redis.call('get',KEY[1])
-- 比较线程标识与取出的标识是否一致
if(id == ARGV[1]) then
-- 释放锁
return redis.call('del',KEY[1])
end
return 0
调用lua脚本的代码如下
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
//静态代码块加载lua
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());
}
不足
虽然上面的基于 reids 中 setnx 实现了分布式锁,且具有原子性,但仍然在实际用途中有着缺陷。
基于我们自主设计的分布式锁仍然存在诸多不足,我们可以选择市场上已经成熟的框架,诸如Redisson
引入依赖
<dependency>
<groupId>org.redissongroupId>
<artifactId>redissonartifactId>
<version>3.13.6version>
配置Redisson客户端
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient(){
//配置
Config config = new Config();
config.useSingleServer().setAddress("redis://43.138.40.82:6379").setPassword("021017");
//创建redissonclient对象
return Redisson.create(config);
}
}
使用redis分布式锁
@Resource
private RedissonClient redissonClient;
@Test
void testRedisson() throws InterruptedException {
// 获取锁(可重入),指定锁的名称
RLock lock = redissonClient.getLock("anyLock");
// 尝试获取锁,参数分别是:获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位
boolean isLock = lock.tryLock(1, 10, TimeUnit.SECONDS);
// 判断释放获取成功
if(isLock){
try {
System.out.println("执行业务");
}finally {
// 释放锁
lock.unlock();
}
}
}
采用hash类型:key中存入锁的标识,然后在field中存入线程标识,用value表示取出的次数。在每次取出时先先验证跟field中存的线程是否一致。如果一致则将value值加1,否则返回false(底层都是由lua脚本实现)
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
internalLockLeaseTime = unit.toMillis(leaseTime);
return evalWriteAsync(getName(), LongCodec.INSTANCE, command,
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"return redis.call('pttl', KEYS[1]);",
Collections.singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}
local key = KEYS[1]; -- 锁的key
local threadId = ARGV[1]; -- 线程唯一标识
local releaseTime = ARGV[2]; -- 锁的自动释放时间
-- 判断是否存在
if(redis.call('exists', key) == 0) then
-- 不存在, 获取锁
redis.call('hset', key, threadId, '1');
-- 设置有效期
redis.call('expire', key, releaseTime);
return 1; -- 返回结果
end;
-- 锁已经存在,判断threadId是否是自己
if(redis.call('hexists', key, threadId) == 1) then
-- 不存在, 获取锁,重入次数+1
redis.call('hincrby', key, threadId, '1');
-- 设置有效期
redis.call('expire', key, releaseTime);
return 1; -- 返回结果
end;
return 0; -- 代码走到这里,说明获取锁的不是自己,获取锁失败
其中删除部分为
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
return evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
"return nil;" +
"end; " +
"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
"if (counter > 0) then " +
"redis.call('pexpire', KEYS[1], ARGV[2]); " +
"return 0; " +
"else " +
"redis.call('del', KEYS[1]); " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; " +
"end; " +
"return nil;",
Arrays.asList(getName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
}
local key = KEYS[1]; -- 锁的key
local threadId = ARGV[1]; -- 线程唯一标识
local releaseTime = ARGV[2]; -- 锁的自动释放时间
-- 判断当前锁是否还是被自己持有
if (redis.call('HEXISTS', key, threadId) == 0) then
return nil; -- 如果已经不是自己,则直接返回
end;
-- 是自己的锁,则重入次数-1
local count = redis.call('HINCRBY', key, threadId, -1);
-- 判断是否重入次数是否已经为0
if (count > 0) then
-- 大于0说明不能释放锁,重置有效期然后返回
redis.call('EXPIRE', key, releaseTime);
return nil;
else -- 等于0说明可以释放锁,直接删除
redis.call('DEL', key);
return nil;
end;