描述
多个客户端获取锁,只有一个客户端能获取到,获取到客户端执行业务逻辑,执行之后释放锁,而未获取到锁的客户断一直等待去获取锁。
可重入锁实现
锁的实现基于lua脚本。
1、上锁:先判断这个锁是否存在,不存在直接创建并设置过期时间,存在value自增1。使用线程id作为获取到锁的key作为表示,好处是防止不同客户端的锁误删。
2、解锁:判断key是否存在,不存在直接返回nil;存在value值进行减1,自减后的值为0时则把锁给删除掉。
3、锁的过期时间续期:key存在就将过期时间就行恢复,定时任务定时执行去恢复key的过期时间。(续期操作的意义,防止业务逻辑还没执行完锁就过期)
public class DistributedRedisLock implements Lock {
private StringRedisTemplate stringRedisTemplate;
private String lockName;
private String uuid;
private long expire = 30;
public DistributedRedisLock(StringRedisTemplate stringRedisTemplate,String lockName,String uuid){
this.stringRedisTemplate = stringRedisTemplate;
this.lockName = lockName;
this.uuid = "uuid:"+Thread.currentThread().getId();
}
@Override
public void lock() {
tryLock();
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public boolean tryLock() {
try {
return this.tryLock(-1L, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
if (time != -1){
this.expire = unit.toSeconds(time);
}
String lua = "if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists', KEYS[1], ARGV[1]) == 1 "+
"then redis.call('hincrby',KEYS[1],ARGV[1],1) redis.call('expire',KEYS[1],ARGV[2]) return 1 "+
"else return 0 end";
while (!stringRedisTemplate.execute(new DefaultRedisScript<>(lua, Boolean.class), Arrays.asList(lockName), uuid, String.valueOf(expire))){
Thread.sleep(50);
}
//获取锁成功后,过期时间自动续期
renewExpire();
return true;
}
@Override
public void unlock() {
String lua = "if redis.call('hexists', KEYS[1], ARGV[1]) == 0 " +
"then " +
" return nil " +
"elseif redis.call('hincrby', KEYS[1], ARGV[1], -1) == 0 " +
"then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
Long flag = stringRedisTemplate.execute(new DefaultRedisScript<>(lua, Long.class), Arrays.asList(lockName), uuid);
if (flag == null ){
throw new IllegalMonitorStateException("this lock doesn't belong to you!");
}
}
@Override
public Condition newCondition() {
return null;
}
public void renewExpire(){
String lua = "if redis.call('hexists',KEYS[1],ARGV[1]) == 1 "+
"then return redis.call('expire',KEYS[1],ARGV[2]) "+
"else return 0 end";
new Timer().schedule(new TimerTask() {
@Override
public void run() {
if (stringRedisTemplate.execute(new DefaultRedisScript<>(lua,Boolean.class),Arrays.asList(lockName),uuid,String.valueOf(expire))) {
renewExpire();
}
}
},expire*1000/3);
}
}
工厂模式调用锁
@Component
public class DistributedLockClient {
@Resource
private StringRedisTemplate stringRedisTemplate;
private String uuid;
public DistributedLockClient() {
}
public DistributedLockClient(String uuid) {
this.uuid = UUID.randomUUID().toString();
}
public DistributedRedisLock getDistributedRedisLock(String lockName){
return new DistributedRedisLock(stringRedisTemplate,lockName,uuid);
}
}
使用可重入锁解决秒杀案例
public void deduct(){
//上锁
DistributedRedisLock lock = distributedLockClient.getDistributedRedisLock("lock");
lock.lock();
try {
String stock = redisTemplate.opsForValue().get("stock");
if (Objects.nonNull(stock)&&Integer.parseInt(stock)>0){
int i = Integer.parseInt(stock);
redisTemplate.opsForValue().set("stock",String.valueOf(--i));
}
test();
}finally {
lock.unlock();
}
}
public void test(){
DistributedRedisLock lock = distributedLockClient.getDistributedRedisLock("lock");
lock.lock();
System.out.println("获取锁成功");
lock.unlock();
}
lua基本语法
a = 5 -- 全局变量
local b = 5 -- 局部变量, redis只支持局部变量
a, b = 10, 2*x -- 等价于 a=10; b=2*x
if( 布尔表达式 1)
then
--[ 在布尔表达式 1 为 true 时执行该语句块 --]
elseif( 布尔表达式 2)
then
--[ 在布尔表达式 2 为 true 时执行该语句块 --]
else
--[ 如果以上布尔表达式都不为 true 则执行该语句块 --]
end
redis执行lua脚本
EVAL script numkeys key [key ...] arg [arg ...]
script:lua脚本字符串,这段Lua脚本不需要(也不应该)定义函数。
numkeys:lua脚本中KEYS数组的大小
key [key ...]:KEYS数组中的元素
arg [arg ...]:ARGV数组中的元素
实例
EVAL "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 5 10 20 30 40 50 60 70 80 90
# 输出:10 20 60 70