继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

分布式锁实现大型连续剧之(一):Redis

青春有我
关注TA
已关注
手记 1072
粉丝 205
获赞 1007

前言:

单机环境下我们可以通过JAVA的Synchronized和Lock来实现进程内部的锁,但是随着分布式应用和集群环境的出现,系统资源的竞争从单进程多线程的竞争变成了多进程的竞争,这时候就需要分布式锁来保证。

实现分布式锁现在主流的方式大致有以下三种

基于数据库的索引和行锁

基于Redis的单线程原子操作:setNX

基于Zookeeper的临时有序节点

这篇文章我们用Redis来实现,会基于现有的各种锁实现来分析,最后分享Redission的锁源码分析来看下分布式锁的开源实现

设计实现

加锁

一、 通过setNx和getSet来实现

这是现在网上大部分版本的实现方式,笔者之前项目里面用到分布式锁也是通过这样的方式实现

public boolean lock(Jedis jedis, String lockName, Integer expire) {

//返回是否设置成功//setNx加锁long now = System.currentTimeMillis(); boolean result = jedis.setnx(lockName,String.valueOf(now + expire *1000)) ==1;if(!result) {//防止死锁的容错Stringtimestamp = jedis.get(lockName);if(timestamp !=null&& Long.parseLong(timestamp) < now) {//不通过del方法来删除锁。而是通过同步的getSetStringoldValue = jedis.getSet(lockName,String.valueOf(now + expire));if(oldValue !=null&& oldValue.equals(timestamp)) {             result =true;             jedis.expire(lockName, expire);         }     } }if(result) {     jedis.expire(lockName, expire); }returnresult;

}

代码分析:

通过setNx命令老保证操作的原子性,获取到锁,并且把过期时间设置到value里面

通过expire方法设置过期时间,如果设置过期时间失败的话,再通过value的时间戳来和当前时间戳比较,防止出现死锁

通过getSet命令在发现锁过期未被释放的情况下,避免删除了在这个过程中有可能被其余的线程获取到了锁

存在问题

防止死锁的解决方案是通过系统当前时间决定的,不过线上服务器系统时间一般来说都是一致的,这个不算是严重的问题

锁过期的时候可能会有多个线程执行getSet命令,在竞争的情况下,会修改value的时间戳,理论上来说会有误差

锁无法具备客户端标识,在解锁的时候可能被其余的客户端删除同一个key

虽然有小问题,不过大体上来说这种分布式锁的实现方案基本上是符合要求的,能够做到锁的互斥和避免死锁

二、 通过Redis高版本的原子命令

jedis的set命令可以自带复杂参数,通过这些参数可以实现原子的分布式锁命令

jedis.set(lockName, "", "NX", "PX", expireTime);

复制代码代码分析

redis的set命令可以携带复杂参数,第一个是锁的key,第二个是value,可以存放获取锁的客户端ID,通过这个校验是否当前客户端获取到了锁,第三个参数取值NX/XX,第四个参数 EX|PX,第五个就是时间

NX:如果不存在就设置这个key XX:如果存在就设置这个key

EX:单位为秒,PX:单位为毫秒

这个命令实质上就是把我们之前的setNx和expire命令合并成一个原子操作命令,不需要我们考虑set失败或者expire失败的情况

解锁

一、 通过Redis的del命令

public boolean unlock(Jedis jedis, String lockName) {

jedis.del(lockName);

return true;

}

代码分析

通过redis的del命令可以直接删除锁,可能会出现误删其他线程已经存在的锁的情况

二、 Redis的del检查

public static void unlock2(Jedis jedis, String lockKey, String requestId) {

// 判断加锁与解锁是不是同一个客户端

if (requestId.equals(jedis.get(lockKey))) {

// 若在此时,这把锁突然不是这个客户端的,则会误解锁

jedis.del(lockKey);

}

}

代码分析

新增了requestId客户端ID的判断,但由于不是原子操作,在多个进程下面的并发竞争情况下,无法保证安全

三、 Redis的LUA脚本

public static boolean unlock3(Jedis jedis, String lockKey, String requestId) {

Stringscript ="if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";Objectresult = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(""));if(1L == (long) result) {returntrue;  }returnfalse;

}

代码分析

通过Lua脚本来保证操作的原子性,其实就是把之前的先判断再删除合并成一个原子性的脚本命令,逻辑就是,先通过get判断value是不是相等,若相等就删除,否则就直接return

Redission的分布式锁

Redission是redis官网推荐的一个redis客户端,除了基于redis的基础的CURD命令以外,重要的是就是Redission提供了方便好用的分布式锁API

一、 基本用法

RedissonClient redissonClient = RedissonTool.getInstance();

RLock distribute_lock = redissonClient.getLock("distribute_lock");try{booleanresult = distribute_lock.tryLock(3,10, TimeUnit.SECONDS);    }catch(InterruptedException e) {        e.printStackTrace();    }finally{if(distribute_lock.isLocked()) {            distribute_lock.unlock();        }    }

代码流程

通过redissonClient获取RLock实例

tryLock获取尝试获取锁,第一个是等待时间,第二个是锁的超时时间,第三个是时间单位

执行完业务逻辑后,最终释放锁

二、 具体实现

我们通过tryLock来分析redission分布式的实现,lock方法跟tryLock差不多,只不过没有最长等待时间的设置,会自旋循环等待锁的释放,直到获取锁为止

long time = unit.toMillis(waitTime);

long current = System.currentTimeMillis();

//获取当前线程ID,用于实现可重入锁

final long threadId = Thread.currentThread().getId();

//尝试获取锁

Long ttl = tryAcquire(leaseTime, unit, threadId);

// lock acquired

if (ttl == null) {



作者:Java高级架构师
链接:https://www.jianshu.com/p/1dcd07f93849


打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP