(阅读本文需要有使用redis/lua的基础)

目录

1.单实例redis:

1.1获取读锁

 1.2获取写锁

 2.redis集群版:

2.1获取读锁

 2.2 删除读锁

2.3 获取写锁

2.4 删除写锁

3.在java项目中的调用:


项目中常常有使用分布式锁的需求,以redis方式实现最为常见和方便。项目中用到以下两种读写锁:

1.redis的setnx方式适用于大部分的写锁,在redis是单实例时,可以读锁key和写锁key结合起来用lua来实现读写锁。

2.当线上redis为多实例的集群时,由于不同的key可能分布在不同的实例上,没法在lua中对不同的key进行操作,因此以hash这种数据结构来做读写锁

本文介绍的两种读写锁实现方式比较:

单实例版集群版
读锁可重入支持支持
锁只能被当前线程删除不支持支持
超时自动释放支持支持
写锁当前线程可重入不支持        不支持

单实例版是早期项目中使用的方式,后来redis扩容和业务更加复杂后无法满足需求,升级为了集群版。此处推荐直接使用集群版

下面将这两种方式的实现做详细介绍:

1.单实例redis:

1.1获取读锁

读锁是可重入锁,在写锁不存在时,可以获得读锁。返回1表示获取成功,0表示失败。

参数说明

key1 读锁key

key2 写锁key

ARGV1 超时时间(秒)

eval "local readKey ,writeKey,timeout = KEYS[1],KEYS[2],ARGV[1] 
local wExist = redis.call('EXISTS',writeKey) 
if wExist == 1 then  
    return 0 
else    
    redis.call('SET',readKey,'1','EX',timeout) 
end 
return 1" 2 readKey writeKey 10

 1.2获取写锁

写锁不可重入,当没有读锁,且设置写锁成功时,返回1获取成功

eval "local readKey ,writeKey,timeout,result  = KEYS[1],KEYS[2],ARGV[1] 
local rExist = redis.call('EXISTS',readKey) 
if rExist == 1 then  
    return 0 
else   
    result = redis.call('SET',writeKey,'1','EX',timeout,'NX')   
    if result  then    
         return 1   
    else return 0   
    end  
end" 2 readKey writeKey 10

此种在集群部署的redis中使用存在的问题:

1.读锁用的key和写锁用的key不在同一个redis分片上时,无法执行lua脚本

2.读线程A可能会删除读线程B设置的读锁

 2.redis集群版:

使用redis的hash结构,保证同一个key在同一个redis实例上。hash中储存read和write两个key,值均为毫秒级时间戳,删除锁的时候对比时间戳是否一致,以保证当前锁只能由当前线程删除或者自动过期。

2.1获取读锁


调用时需要传入的参数说明:

key1 锁的key

ARGV1 key1的过期时间

ARGV2 读锁的超时时间

ARGV3 写锁的超时时间

ARGV4 当前毫秒级时间戳


local lockKey,timeout,readExpTime,writeExpTime,currentTimeWs = KEYS[1],tonumber(ARGV[1]),tonumber(ARGV[2]),tonumber(ARGV[3]),tonumber(ARGV[4]) 
local writeValue = redis.call('HGET',lockKey,'write') 
local canGet=false
if writeValue then
   canGet=((currentTimeWs - tonumber(writeValue))> writeExpTime )
else canGet=true 
end 
if canGet  then 
   redis.call('HSET',lockKey,'read',currentTimeWs) 
   redis.call('PEXPIRE',lockKey,timeout)
   return 1
else
   return 0
end

 2.2 删除读锁

删除的时候比较read的值

local lockKey,readTime = KEYS[1],ARGV[1]
local readValue = redis.call('HGET',lockKey,'read')
if readTime==readValue then
    redis.call('HDEL',lockKey,'read')
end

2.3 获取写锁

获取写锁时要同时判断读锁和写锁是否存在或者已超时

local lockKey,timeout,readExpTime,writeExpTime,currentTimeWs = KEYS[1],tonumber(ARGV[1]),tonumber(ARGV[2]),tonumber(ARGV[3]),tonumber(ARGV[4]) 
local writeValue = redis.call('HGET',lockKey,'write') 
local writeValid=false
if writeValue then
   writeValid=((currentTimeWs - tonumber(writeValue))> writeExpTime )
else
   writeValid=true 
end 
local readValue = redis.call('HGET',lockKey,'read') 
local readValid=false
if readValue then
   readValid=((currentTimeWs - tonumber(readValue))> readExpTime )
else
   readValid=true 
end 
if writeValid and readValid  then 
   redis.call('HSET',lockKey,'write',currentTimeWs) 
   redis.call('PEXPIRE',lockKey,timeout)
   return 1
else
   return 0
end

2.4 删除写锁

同样的,要判断write的值是否一致

local lockKey,writeTime = KEYS[1],ARGV[1]
local writeValue = redis.call('HGET',lockKey,'write')
if writeTime==writeValue then
    redis.call('HDEL',lockKey,'write')
end

3.在java项目中的调用:

private static final String READ_LOCK_SCRIPT = "local lockKey,timeout,readExpTime,writeExpTime,currentTimeWs = KEYS[1],tonumber(ARGV[1]),tonumber(ARGV[2]),tonumber(ARGV[3]),tonumber(ARGV[4])  local writeValue = redis.call('HGET',lockKey,'write')  local canGet=false if writeValue then canGet=((currentTimeWs - tonumber(writeValue))> writeExpTime ) else canGet=true  end  if canGet  then  redis.call('HSET',lockKey,'read',currentTimeWs)  redis.call('PEXPIRE',lockKey,timeout) return 1 else return 0 end ";
 
private static final String WRITE_LOCK_SCRIPT = "local lockKey,timeout,readExpTime,writeExpTime,currentTimeWs = KEYS[1],tonumber(ARGV[1]),tonumber(ARGV[2]),tonumber(ARGV[3]),tonumber(ARGV[4])  local writeValue = redis.call('HGET',lockKey,'write')  local writeValid=false if writeValue then writeValid=((currentTimeWs - tonumber(writeValue))> writeExpTime ) else writeValid=true  end  local readValue = redis.call('HGET',lockKey,'read')  local readValid=false if readValue then readValid=((currentTimeWs - tonumber(readValue))> readExpTime ) else readValid=true  end  if writeValid and readValid  then  redis.call('HSET',lockKey,'write',currentTimeWs)  redis.call('PEXPIRE',lockKey,timeout) return 1 else return 0 end";
 
private static final String DEL_READ_LOCK_SCRIPT = "local lockKey,readTime = KEYS[1],ARGV[1] local readValue = redis.call('HGET',lockKey,'read') if readTime==readValue then redis.call('HDEL',lockKey,'read') end";
private static final String DEL_WRITE_LOCK_SCRIPT = "local lockKey,writeTime = KEYS[1],ARGV[1] local writeValue = redis.call('HGET',lockKey,'write') if writeTime==writeValue then redis.call('HDEL',lockKey,'write') end";
 
private static Integer LOCK_KEY_TIMEOUT = 10 * 1000;
private static Integer LOCK_READ_KEY_TIMEOUT = 1 * 1000;
private static Integer LOCK_WRITE_KEY_TIMEOUT = 3 * 1000;
private String getReadLock(String lockKey,Integer lockTimeOut,Integer readTimeOut, Integer writeTimeOut){
    RedisTemplete redis = routeRedis(lockKey);//根据key路由出redis客户端
    String value = String.valueOf(System.currentTimeMillis());
 
    List<String> args = new ArrayList<>(4);
    args.add(lockTimeOut.toString());
    args.add(readTimeOut.toString());
    args.add(writeTimeOut.toString());
    args.add(value);
 
    Long result = (Long) redis.eval(READ_LOCK_SCRIPT, lockKey, args);
 
    if(Long.valueOf(1).equals(result)){
        return value;
    }
    return null;
}
private String getWriteLock(String lockKey,Integer lockTimeOut,Integer readTimeOut, Integer writeTimeOut){
    RedisTemplete redis = routeRedis(lockKey);//根据key路由出redis客户端

    List<String> keys = new ArrayList<>(1);
    keys.add(lockKey);
 
    String value = String.valueOf(System.currentTimeMillis());
 
    List<String> args = new ArrayList<>(4);
    args.add(lockTimeOut.toString());
    args.add(readTimeOut.toString());
    args.add(writeTimeOut.toString());
    args.add(value);
    Long result = (Long) redis.eval(WRITE_LOCK_SCRIPT, keys, args);
    if(Long.valueOf(1).equals(result)){
        return value;
    }
    return null;
}
private void delWriteLock(String lockKey,String value){
    RedisTemplete redis = routeRedis(lockKey);//根据key路由出redis客户端

    List<String> keys = new ArrayList<>(1);
    keys.add(lockKey);
 
    List<String> args = new ArrayList<>(1);
    args.add(value);
    redis.eval(DEL_WRITE_LOCK_SCRIPT, keys, args);
}
private void delReadLock(String lockKey,String value){
    RedisTemplete redis = routeRedis(lockKey);//根据key路由出redis客户端

    List<String> keys = new ArrayList<>(1);
    keys.add(lockKey);
 
    List<String> args = new ArrayList<>(1);
    args.add(value);
    redis.eval(DEL_READ_LOCK_SCRIPT, keys, args);
}

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐