我们在编程有很多场景使用本地锁和分布式锁,但是是否考虑这些锁的原理是什么?本篇讨论下实现分布式锁的常见办法及他们实现原理。

一、使用锁的原则

使用本地锁和分布式锁是为了解决并发导致脏数据的场景,使用锁的最高境界是通过流程设计避免使用锁,锁会牺牲掉系统性能为代价的。

1、常见的分布式锁实现

常见分布式锁产品性能:redis>zookeeper>mysql,获取锁成功率:mysql悲观>zk>redis

锁实现

实现方式

性能

选型注意

选择关注点

mysql

乐观锁

并发场景锁无效

低成本实现

悲观锁

可能导致锁表

极端场景

zk

顺序节点

性能、可靠一般

性能、可靠的兼顾选择

redis

setNx

锁没有唯一标示

简单但不推荐

lua脚本

最高

用不好效果更差

大神选用不是大神用redisson

redisson

中高

平衡做的好

关注性能

二、MySQL实现分布式锁原理

1、乐观锁实现方式

通过在mysql加入version或者updatetime时间戳的方式实现。下面主要介绍新增流程,修改的更简单不做介绍。乐观锁实现出现事务回滚的时候要处理掉新增场景中无效数据。乐观锁其实并没有用到锁的概念,其实他是一个版本同步的技巧实现。
version-新增实现:在数据库新增的时候先新增一条数据,然后读取这个数据返回给修改页面,当修改页面提交的时候version对比如果一致说明数据合法,如果不一致说明数据不合法。然后version自增写入数据库。
updatetime-新增实现:同version规则一致。
乐观锁无法解决的问题:乐观锁无法解决并发多线程问题,这种适合解决并发比较低的场景的数据库数据一致性问题。

2、悲观锁实现

悲观锁实现:悲观锁实现简单 select * from table for update,悲观锁是所有锁中理论最靠谱的一种,但是性能差。在并发场景不推荐使用;
悲观锁的问题:性能问题、导致锁表

三、ZooKeeper实现分布式锁原理

1、Curator Recipes实现哪些锁

  • InterProcessMutex:可重入、独占锁InterProcessSemaphoreMutex:不可重入、独占锁
  • InterProcessReadWriteLock:读写锁
  • InterProcessSemaphoreV2 : 共享信号量
  • InterProcessMultiLock:多重共享锁 (将多个锁作为单个实体管理的容器)

2、ZooKeeper分布式锁实现

<!-- zookeeper curator客户端  -->
<dependency><groupId>org.apache.curator</groupId><artifactId>curator-recipes</artifactId><version>2.12.0</version>
</dependency>/*** 功能:zk - zk Curator客户端 - 实现分布式锁测试* 作者:丁志超*/
public class ZkCuratorLock{  //实例化客户端private static RetryPolicy retryPolicy  = new ExponentialBackoffRetry(1000,3);private static CuratorFramework client = CuratorFrameworkFactory.builder().connectString("ip:2181").sessionTimeoutMs(3000).connectionTimeoutMs(5000).retryPolicy(retryPolicy).build();//zk分布式锁创建节点在零时目录zklock下创建static String lockPath = "/zklock";//实例化分布式锁final static InterProcessLock lock = new InterProcessSemaphoreMutex(client, lockPath);public static void main(String[] args) {//获取锁try {lock.acquire();} catch (Exception e) {// TODO Auto-generated catch blocke.printStackTrace();}finally {//释放锁try {lock.release();} catch (Exception e) {}}}} 

3、ZooKeeper分布式事务原理

ZooKeeper实现分布式事务的原理,是基于ZooKeeper顺序节点特性实现的。

  • 创建一个锁目录lock
  • 线程A获取锁会在lock目录下,创建临时顺序节点
  • 获取锁目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小获得锁
  • 线程B创建临时节点并获取所有兄弟节点,只监听当前获取锁的节点的变化,节约效率。
  • 线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是最小的节点获得锁

由于节点的临时属性,如果创建znode的那个客户端崩溃了,那么相应的znode会被自动删除。这样就避免了设置过期时间的问题,Curator客户端是基于顺序节点实现的分布式锁。

4、ZooKeeper分布式锁面临问题

1、是否有脑裂问题?

由于zookeeper是基于Ha方式部署的,其写全部在主节点进行,只要主节点服务正常就不会出现脑裂问题。

2、心跳超时问题?

由于zookeeper客户端与服务端session保持会话需要心跳机制保证。如果客户端或者服务端GC导致心跳超时,此时零时节点会被zookeeper全部踢掉。zookeeper的零时节点绑定在会话session上面,session不存在客户端创建的零时节点全部会被删除。

5、ZooKeeper Curator实现分布式锁源码分析

//1、Curator锁数据结构
private static class LockData
{//当前拥有锁的线程final Thread owningThread;//当前锁的路径final String lockPath;//锁计数器final AtomicInteger lockCount = new AtomicInteger(1);}//2、这段是Curator的精华 - 获取锁成功后验证 及 获取锁失败后等待
private boolean internalLockLoop(long startMillis, Long millisToWait, String ourPath) throws Exception{boolean     haveTheLock = false;boolean     doDelete = false;try{if ( revocable.get() != null ){client.getData().usingWatcher(revocableWatcher).forPath(ourPath);}while ( (client.getState() == CuratorFrameworkState.STARTED) && !haveTheLock ){List<String>        children = getSortedChildren();String              sequenceNodeName = ourPath.substring(basePath.length() + 1); // +1 to include the slash//成功获取锁PredicateResults    predicateResults = driver.getsTheLock(client, children, sequenceNodeName, maxLeases);if ( predicateResults.getsTheLock() ){haveTheLock = true;}else{//没有获取锁,监听拥有锁节点的变化String  previousSequencePath = basePath + "/" + predicateResults.getPathToWatch();//获取锁失败后等待超时如果不设置超时一直等待synchronized(this){try {// use getData() instead of exists() to avoid leaving unneeded watchers which is a type of resource leakclient.getData().usingWatcher(watcher).forPath(previousSequencePath);if ( millisToWait != null ){millisToWait -= (System.currentTimeMillis() - startMillis);startMillis = System.currentTimeMillis();if ( millisToWait <= 0 ){doDelete = true;    // timed out - delete our nodebreak;}wait(millisToWait);}else{wait();}}catch ( KeeperException.NoNodeException e ) {// it has been deleted (i.e. lock released). Try to acquire again}}}}}catch ( Exception e ){ThreadUtils.checkInterrupted(e);doDelete = true;throw e;}finally{if ( doDelete ){deleteOurPath(ourPath);}}return haveTheLock;}//3、获取锁算法思想 - maxLeases默认值是1,要求获取锁的线程永远是list的第一个线程,保证获取锁顺序性
public PredicateResults getsTheLock(CuratorFramework client, List<String> children, String sequenceNodeName, int maxLeases) throws Exception{int             ourIndex = children.indexOf(sequenceNodeName);validateOurIndex(sequenceNodeName, ourIndex);boolean         getsTheLock = ourIndex < maxLeases;String          pathToWatch = getsTheLock ? null : children.get(ourIndex - maxLeases);return new PredicateResults(pathToWatch, getsTheLock);}

四、Redis实现分布式锁

1、Redisson支持哪些分布式锁

  • RedissonLock 功能常见常用的获取非公平锁的类
  • RedissonMultiLock 功能是一个方法有很多锁构成,把这些锁统一管理
  • RedissonSpinLock 功能是自旋方式获取锁的RedissonReadLock
  • RedissonWriteLock 功能redisson实现的读写锁 读实现:hget 写:setNx
  • RedissonRedLock 功能与RedissonMultiLock相似

2、Redis分布式锁实现

1、Redis SetNx实现

//setNx实现分布式锁
public class SetNxLock {public static void main(String[] args) {Jedis jedis = new Jedis("localhost");jedis.setnx("key", "value");try {if(jedis.exists("key")) {jedis.expire("key", 10);System.out.println("我获取了锁,干点活!");jedis.del("key");}} catch (Exception e) {} finally {jedis.del("key");}}
}  

2、Redis lua脚本实现

/*** 功能:redis - lua - lua实现分布式锁 使用redis.clients客户端* 作者:丁志超*/
public class LuaLock {public static void main(String[] args) {lock("122333", "33331","10000" );unlock("122333", "33331");}/*** 加锁语法* key:redis key* value:redis value* time: redis timeouts 锁过期时间一般大于最耗时的业务消耗的时间 * 语法参考文档:https://www.runoob.com/redis/redis-scripting.html* */    public static String lock(String key, String value,String timeOut ) {/***  -- 加锁脚本,其中KEYS[]为外部传入参数*  -- KEYS[1]表示key *  -- ARGV[1]表示value*  -- ARGV[2]表示过期时间*/String lua_getlock_script = "if redis.call('SETNX','"+key+"','"+value+"') == 1 then" +"     return redis.call('pexpire','"+key+"','"+timeOut+"')" +" else" +"     return 0 " +"end";Jedis jedis = new Jedis("localhost");//在缓存中添加脚本但不执行String scriptId = jedis.scriptLoad(lua_getlock_script);//查询脚本是否添加Boolean isExists = jedis.scriptExists(scriptId);//执行脚本 返回1表示成功,返回0表示失败Object num = jedis.eval(lua_getlock_script);;return String.valueOf(num);}/*** 释放锁语法* key:redis key* value:redis value* time: redis timeouts 锁过期时间一般大于最耗时的业务消耗的时间 * 语法参考文档:https://www.runoob.com/redis/redis-scripting.html* */ public static String unlock(String key, String value ) {/***  -- 加锁脚本,其中KEYS[]为外部传入参数*  -- KEYS[1]表示key *  -- ARGV[1]表示value*  -- ARGV[2]表示过期时间*/String lua_unlock_script  ="if redis.call('get','"+key+"') == '"+value+"' then " +" return redis.call('del','"+key+"') " +"else  return 0 " +"end";Jedis jedis = new Jedis("localhost");//在缓存中添加脚本但不执行String scriptId = jedis.scriptLoad(lua_unlock_script);//查询脚本是否添加Boolean isExists = jedis.scriptExists(scriptId);//执行脚本 返回1表示成功,返回0表示失败Object num = jedis.eval(lua_unlock_script);;return String.valueOf(num);}
}

3、Redis Redisson实现

/*** 功能:redis - Redisson - Redisson实现分布式锁* 作者:丁志超*/
public class RedissonLock {public static void main(String[] args) {Config config = new Config(); config.useSingleServer().setAddress("localhost");RedissonClient redissonClient = Redisson.create(config);RLock rLock = redissonClient.getLock("key");try {rLock.tryLock(10, TimeUnit.SECONDS);System.out.println("我获取了锁,该我干活了。");} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}finally {if(rLock.isLocked()) {rLock.unlock();}}}
}  

3、单节点redis分布式实现原理

1、单节点setNx、lua脚本分布式锁实现原理

setNx线程模型:setNx线程模型必须是单线程(包括接收、解析、处理线程),才能保证其没有并发问题。这个猜想没有找到源码及相关文档的证明。redis线程模型文章

SET resource_name my_random_value NX PX 30000

lua脚本:lua脚本其实就是一个类似于mysql的存储过程,其最大的意义是降低网络交互。性能要优于单独使用redis命令。

if redis.call("get",KEYS[1]) == ARGV[1] thenreturn redis.call("del",KEYS[1])
elsereturn 0
end

2、Redisson分布式锁实现原理

//redisson锁实体数据结构
public class RedissonLockEntry  {//计数器private int counter;//信号类 控制多少线程同时获取锁private final Semaphore latch;private final RPromise<RedissonLockEntry> promise;//线程队列private final ConcurrentLinkedQueue<Runnable> listeners = new ConcurrentLinkedQueue<Runnable>();
}//源码类位置 redisson RedissonLock.class
//redisson实现分布式锁源码解析
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {long time = unit.toMillis(waitTime);long current = System.currentTimeMillis();long threadId = Thread.currentThread().getId();//尝试获取锁其实现见后面的方法Long ttl = tryAcquire(waitTime, leaseTime, unit, threadId);// lock acquiredif (ttl == null) {return true;}//如果锁超时直接返回失败time -= System.currentTimeMillis() - current;if (time <= 0) {acquireFailed(waitTime, unit, threadId);return false;}current = System.currentTimeMillis();   //通过线程ID获取锁结构体RFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);if (!subscribeFuture.await(time, TimeUnit.MILLISECONDS)) {if (!subscribeFuture.cancel(false)) {subscribeFuture.onComplete((res, e) -> {if (e == null) {unsubscribe(subscribeFuture, threadId);}});}acquireFailed(waitTime, unit, threadId);return false;}try {//再次检查锁是否超时,如果超时释放锁time -= System.currentTimeMillis() - current;if (time <= 0) {acquireFailed(waitTime, unit, threadId);return false;}//在非超时周期内通过自旋方式获取锁while (true) {long currentTime = System.currentTimeMillis();ttl = tryAcquire(waitTime, leaseTime, unit, threadId);// lock acquiredif (ttl == null) {return true;}//超时释放锁time -= System.currentTimeMillis() - currentTime;if (time <= 0) {acquireFailed(waitTime, unit, threadId);return false;}// waiting for messagecurrentTime = System.currentTimeMillis();if (ttl >= 0 && ttl < time) {subscribeFuture.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);} else {subscribeFuture.getNow().getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);}time -= System.currentTimeMillis() - currentTime;if (time <= 0) {acquireFailed(waitTime, unit, threadId);return false;}}} finally {unsubscribe(subscribeFuture, threadId);}
//        return get(tryLockAsync(waitTime, leaseTime, unit));}//redisson获取锁最底层实现,使用lua脚本实现,如果有key返回key的剩余生命时间
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {return evalWriteAsync(getRawName(), LongCodec.INSTANCE, command,//若 key 存在返回 1 ,否则返回 0               "if (redis.call('exists', KEYS[1]) == 0) then " +//给key增加超时时间    "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +//设置key的生命周期      "redis.call('pexpire', KEYS[1], ARGV[1]); " +"return nil; " +"end; " +//查询key存在      "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(getRawName()), unit.toMillis(leaseTime), getLockName(threadId));
} //释放锁
@Override
protected RFuture<Void> acquireFailedAsync(long waitTime, TimeUnit unit, long threadId) {long wait = threadWaitTime;if (waitTime != -1) {wait = unit.toMillis(waitTime);}//这块看的有点懵,等学习lua在看return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_VOID,// 移除list超时的key及对应的线程"local queue = redis.call('lrange', KEYS[1], 0, -1);" +// find the location in the queue where the thread is"local i = 1;" +"while i <= #queue and queue[i] ~= ARGV[1] do " +"i = i + 1;" +"end;" +// go to the next index which will exist after the current thread is removed"i = i + 1;" +// decrement the timeout for the rest of the queue after the thread being removed"while i <= #queue do " +"redis.call('zincrby', KEYS[2], -tonumber(ARGV[2]), queue[i]);" +"i = i + 1;" +"end;" +// remove the thread from the queue and timeouts set//移除超时的线程              "redis.call('zrem', KEYS[2], ARGV[1]);" +"redis.call('lrem', KEYS[1], 0, ARGV[1]);",Arrays.<Object>asList(threadsQueueName, timeoutSetName),getLockName(threadId), wait);
}

4、redis看门狗机制

redis看门狗机制就是锁过期时间自动续期的一种自动检查机制,具体涉及下面2个方法。 看门狗机制采用续期的方式保证了方法一定会执行完毕,但是会导致系统的并发大大降低。

1、lock():未指定过期时间,实现时会设置过期时间,默认30s,然后采用Watchdog不断续期,直至释放锁;

2、lock(long leaseTime, TimeUnit unit):指定过期时间,超过有效期时间后,会自动释放锁

public void lock() {try {// 过期时间为-1,表示永不过期lock(-1, null, false);} catch (InterruptedException e) {throw new IllegalStateException();}
}private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {long threadId = Thread.currentThread().getId();Long ttl = tryAcquire(-1, leaseTime, unit, threadId);if (ttl == null) {// 获取到锁直接返回return;}//还未获取到锁// 订阅锁,这样锁释放时会被通知到RFuture<RedissonLockEntry> future = subscribe(threadId);if (interruptibly) {commandExecutor.syncSubscriptionInterrupted(future);} else {commandExecutor.syncSubscription(future);}try {while (true) {ttl = tryAcquire(-1, leaseTime, unit, threadId);if (ttl == null) {// 获取到锁则可以退出死循环break;}if (ttl >= 0) {try {// 指定超时时间内获取future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);} catch (InterruptedException e) {if (interruptibly) {throw e;}future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);}} else {// 未指定超时时间获取if (interruptibly) {future.getNow().getLatch().acquire();} else {future.getNow().getLatch().acquireUninterruptibly();}}}} finally {// 取消锁的订阅unsubscribe(future, threadId);}//    get(lockAsync(leaseTime, unit));
}private Long tryAcquire(long waitTime, long leaseTime, TimeUnit unit, long threadId) {return get(tryAcquireAsync(waitTime, leaseTime, unit, threadId));
}private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {RFuture<Long> ttlRemainingFuture;if (leaseTime != -1) {// 指定过期时间ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);} else {// 未指定过期时间// 过期时间设为看门狗超时时间,然后由看门狗一直续期,直到锁释放ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);}//看门狗实现核心回调scheduleExpirationRenewal自动给锁续期ttlRemainingFuture.onComplete((ttlRemaining, e) -> {if (e != null) {return;}if (ttlRemaining == null) {// 获取到锁if (leaseTime != -1) {internalLockLeaseTime = unit.toMillis(leaseTime);} else {// 未指定过期时间,需要开启Watchdog自动续期scheduleExpirationRenewal(threadId);}}});return ttlRemainingFuture;
}//首先看下尝试获取锁的实现,tryLockInnerAsync方法通过EVAL执行LUA脚本,代码如下:
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {return evalWriteAsync(getRawName(), 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(getRawName()), unit.toMillis(leaseTime), getLockName(threadId));
}

5、Redis集群分布式锁实现原理

上面介绍的setNx、lua实现分布式锁都是基于单节点的redis本地实现方式,只要解决好本地的线程并发问题即可。当时当redis集群中使用分布式锁怎么办?redis集群实现分布式锁基于redlock算法,下面介绍其实现原理及缺陷。

1、redlock实现前提

在算法的分布式版本中,我们假设我们有N个 Redis 主节点,这些节点是完全独立的。上面已经介绍单个节点如何获取释放锁。那么在集群中每台独立的redis也会使用这种方式获取释放锁。我们假设5台redis节点,这个数值不是固定的是业务需要的选择。

2、redlock实现步骤

1、它以毫秒为单位获取当前时间。

2、它尝试顺序获取所有 N 个实例中的锁,在所有实例中使用相同的键名和随机值。在第 2 步中,当在每个实例中设置锁时,客户端使用一个比总锁自动释放时间更小的超时来获取它。例如,如果自动释放时间为 10 秒,则超时可能在 ~ 5-50 毫秒范围内。这可以防止客户端长时间处于阻塞状态,试图与已关闭的 Redis 节点通信:如果实例不可用,我们应该尽快尝试与下一个实例通信。

3、客户端通过从当前时间中减去步骤 1 中获得的时间戳来计算获取锁所用的时间。当且仅当客户端能够在大多数实例(至少 3 个)中获取锁,并且获取锁所用的总时间小于锁有效时间,则认为该锁已获取。

4、如果获得了锁,则其有效时间被认为是初始有效时间减去经过的时间,如步骤 3 中计算的那样。

5、如果客户端由于某种原因获取锁失败(或者它无法锁定 N/2+1 个实例或有效时间为负),它将尝试解锁所有实例(即使是它认为没有锁定的实例)能够锁定)。

3、Redis集群分布式锁实现原理总结

上面是redis官方介绍的,为了保证原汁原味我给翻译下了,下面按照我理解的总结。红色部分是我对官方理解不一致的地方。

1、redis时间精度很高以毫秒为单位获取时间,这点也透露出来redis对系统时间的敏感性,也是马丁质疑他的一个点。

2、redis在N个节点中同时去获取锁,如果在超时周期获取不到锁就释放锁,防止通讯堵塞。这也是马丁质疑他的一点马丁认为这点会导致多通讯次数增加服务器成本。

3、如果客户端能在N个redis节点中在(N/2+1)个节点中获取锁,且获取锁各个节点耗时最久的时间小于锁的有效时间就认为客户端获取了锁。

4、redis获取锁的有效时间等于锁有效时间减去获取锁花费的时间在减去集群节点时间差。怎么理解?木桶效应比如锁生命周期是10秒,最快节点1秒获取,最慢的3秒获取。此时锁生命周期就是 10-(3-1)=8在减去获取锁过程的时间,屏蔽集群的时间差异。

5、无法获取N/2+1 个实例或获取锁超时即获取锁失败。

6、redlock如何保证安全

算法安全吗?我们可以尝试了解在不同场景中会发生什么。首先让我们假设客户端能够在大多数情况下获取锁。所有实例都将包含一个具有相同生存时间的密钥。但是,密钥是在不同的时间设置的,因此密钥也会在不同的时间到期。但是如果第一个键在时间 T1(我们在联系第一台服务器之前采样的时间)设置为最差,而最后一个键在时间 T2(我们从最后一个服务器获得回复的时间)设置为最差,我们肯定集合中第一个过期的键至少会存在MIN_VALIDITY=TTL-(T2-T1)-CLOCK_DRIFT。所有其他密钥将在稍后到期,因此我们确信至少这次密钥将同时设置。

在设置了大部分键的期间,另一个客户端将无法获取锁,因为如果 N/2+1 个键已经存在,则 N/2+1 SET NX 操作将无法成功。因此,如果获取了锁,则不可能同时重新获取它(违反互斥属性)。但是,我们还希望确保多个客户端同时尝试获取锁不能同时成功。

如果客户端使用接近或大于锁最大有效时间(我们基本用于 SET 的 TTL)的时间锁定了大多数实例,它会认为锁无效并解锁实例,因此我们只需要考虑客户端能够在小于有效时间的时间内锁定大多数实例的情况。在这种情况下,对于上面已经表达的参数,MIN_VALIDITY没有客户端应该能够重新获取锁。因此,只有当锁定多数的时间大于 TTL 时间时,多个客户端才能同时锁定 N/2+1 个实例(“时间”为第 2 步的结束),从而使锁定无效。

总结:上面是redis官方对redlok如何保证安全的理解,我这边做个自己理解的注释。想说的就是这个算法MIN_VALIDITY=TTL-(T2-T1)-CLOCK_DRIFT,节点T2-T1这个变量就是为了屏蔽掉集群节点的时间差异,如果得到的结果生命周期小于0或者无法获取N/2+1个节点就认为获取锁失败了。

7、马丁·克莱普曼喷redLock算法

我认为 Redlock 算法是一个糟糕的选择,因为它“既不是鱼也不是家禽”:它对于效率优化锁来说是不必要的重量级和昂贵的,但是对于正确性取决于锁的情况来说它不够安全。

特别是,该算法对时序和系统时钟做出了危险的假设(基本上假设同步系统具有有限的网络延迟和有限的操作执行时间),如果不满足这些假设,则会违反安全属性。此外,它缺乏生成防护令牌的设施(保护系统免受网络长时间延迟或暂停进程的影响)。

如果您只需要尽力而为的锁定(作为效率优化,而不是为了正确性),我建议坚持使用简单的 Redis单节点锁定算法(条件设置如果不存在以获得锁定, atomic delete-if-value-matches 以释放锁),并在您的代码中非常清楚地记录锁只是近似值,有时可能会失败。不要费心设置五个 Redis 节点的集群。

另一方面,如果您需要锁定以确保正确性,请不要使用 Redlock。相反,请使用适当的共识系统,例如ZooKeeper,可能通过 实现锁的Curator 配方之一。(至少,使用具有合理事务保证的数据库。)并且请在锁定下的所有资源访问中强制使用防护令牌。

正如我开头所说的,Redis 是一个很好的工具,如果你使用得当。以上都不会削弱 Redis 对其预期目的的有用性。Salvatore多年来一直致力于该项目,其成功当之无愧。但是每个工具都有局限性,了解它们并相应地进行计划很重要。

总结:马丁·克莱普曼说的也对,比如redis在5台机器中3台已经获取锁,但是其中3台有一台挂了,然后重启redis还是认为成功获取锁。redlock是一种分布式场景解决一致性的方案,其也受制CAP理论。其保证AP可用性、分区容错性。redis是最大的最求性能,这是他一直信奉的原则,redlock在极苛刻的条件下出现问题。但是不代表其不科学,zk也有自己丢数据的场景,mysql一样也有自己丢数据的场景。关键是这个事情发生的概率,所以我个人观点是不怎么支持马丁·克莱普曼的观点。

五、分布式锁性能测试

光说不练说明没有理解,光练不说说明没有系统的看全面,最后给各类分布式锁性能做一个测试,由于受制于测试环境资源,我们测试的值可能和你们的不一样,但是我们追求的是测试方法的科学性而不是绝对值。本次测试用本地单体应用,实际生产环境多个节点要结合自己的环境特点测试。测试代码及压测脚本
测试方式:
1.单应用-本地部署(I5*8G mac)一套应用jmeter压测这个应用
2.redis master集群-4核心8G*3节点
3.zk集群 - 4核心8G*3节点

锁实现

集群方式

实现方式

获取锁成功率

TPS

取样次数

mysql

乐观锁

待测

待测

1-3万次

悲观锁

待测

待测

1-3万次

zk

单节点

curator

100%

1066

1-3万次

集群

curator

100%

50-70

1-3万次

redis

cluster

setNx

100%

100-120

1-3万次

lua脚本

100%

200

1-3万次

redisson

50-100%

1100

1-10万次

哨兵

setNx

待测

待测

1-3万次

lua脚本

待测

待测

1-3万次

redisson

待测

待测

1-3万次

单节点

setNx

待测

待测

1-3万次

lua脚本

待测

待测

1-3万次

redisson

待测

待测

1-3万次

六、分布式锁性能优化思考

分布式锁,能不用就不用用,非用不可要充分的考虑到性能。可以采用以下优化,加入使用redis,可以使用多个redis集群,通过hashkey锁定目标集群。让多个集群提供并发能力。【细节待优化】

七、相关文档
3.1、redis分布式锁实现原理官方文档
3.2、马丁·克莱普曼对redlock质疑

若有收获,就点个赞吧

17、Redis、Zk分布式锁实现原理相关推荐

  1. redis实现分布式锁的原理

    redis实现分布式锁的原理 一.为什么使用分布式锁? ·>本地锁的局限性: ·>分布式锁的概念: 二.redis实现分布式锁的原理? 1.抢占分布式锁: 2.加锁的同时设置过期时间: 3 ...

  2. redis分布式锁与zk分布式锁的对比

    在分布式环境下,传统的jvm级别的锁会失效,那么分布式锁就是非常有必要的一个技术,一般我们可以通过redis,zk等技术来实现我们的分布式锁 redis实现分布式锁: 原理:我们都知道redis的处理 ...

  3. 分布式锁、ZK分布式锁、Redis分布式锁

    常见的分布式锁实现方案:ZK分布式锁.Redis分布式锁 ZK分布式锁: 原理:使用ZK 的临时有序节点.节点的监听机制来实现的. 锁特点:悲观锁,公平锁 获取锁:客户端A在/mylock节点目录下创 ...

  4. Redis实现分布式锁原理(面试重点)

    一.为什么使用分布式锁? >本地锁的局限性(synchronized): 本地锁只能锁住当前服务,只能保证自己的服务,只有一个线程可以访问,但是在服务众多的分布式环境下,其实是有多个线程同时访问 ...

  5. 【Redis学习】Redis实现分布式锁

      目前几乎很多大型网站及应用都是分布式部署的,分布式场景中的数据一致性问题一直是一个比较重要的话题.分布式的CAP理论告诉我们"任何一个分布式系统都无法同时满足一致性(Consistenc ...

  6. 用 Redis 实现分布式锁(Java 版)

    用 Redis 实现分布式锁(Java 版) 核心代码 完整代码   分布式锁是一种解决分布式临界资源并发读写的一种技术.本文详细介绍了在 Java 中使用 Redis 实现分布式锁的方法.为了方便, ...

  7. 老夫带你深度剖析Redisson实现分布式锁的原理

    Redis实现分布式锁的原理 前面讲了Redis在实际业务场景中的应用,那么下面再来了解一下Redisson功能性场景的应用,也就是大家经常使用的分布式锁的实现场景. 引入redisson依赖 < ...

  8. Redis进阶- Redisson分布式锁实现原理及源码解析

    文章目录 Pre 用法 Redisson分布式锁实现原理 Redisson分布式锁源码分析 redisson.getLock(lockKey) 的逻辑 redissonLock.lock()的逻辑 r ...

  9. redis cluster 分布式锁_关于分布式锁原理的一些学习与思考redis分布式锁,zookeeper分布式锁...

    首先分布式锁和我们平常讲到的锁原理基本一样,目的就是确保,在多个线程并发时,只有一个线程在同一刻操作这个业务或者说方法.变量. 在一个进程中,也就是一个jvm 或者说应用中,我们很容易去处理控制,在j ...

最新文章

  1. LINUX进程调度分析源码,Linux 实时调度(源码分析)
  2. 外链引入css有哪些方式_引入CSS样式表的方式有哪些?
  3. 数据蒋堂 | Hadoop中理论与工程的错位
  4. 没有JS的前端:体积更小、速度更快!
  5. 3个月带你通关Go语言
  6. python中扑克牌类设计_Python类的基础设计、使用
  7. python二级简书_12月4日,总结发现杯,备战python二级
  8. [翻译] 学习iOS开发的建议:如何从菜鸟到专家
  9. html 响应式布局 九宫格,两种方法实现响应式九宫格布局
  10. 属性定义为 not null unique_Spring Data with MySQL (实体定义)
  11. 150W光速秒充!realme真我GT Neo3正式发布 售价1999元起
  12. php装饰器模式 简书,装饰器模式/包装器模式
  13. 4.5_abstract_factory_创建型模式:抽象工厂模式
  14. java打印空心金字塔
  15. “拼多多优惠券”测试的套路,今天让你秒懂~
  16. mysql 存储过程的使用;
  17. 创客使用Fusion 360 - 认识Fusion 360
  18. 计算机在足球中的应用,人工智能在机器人足球赛中的应用
  19. linux远程桌面太卡,确保远程桌面管理顺畅稳定的方法
  20. 《tkinter实用教程二》tkinter的子模块ttk

热门文章

  1. 学生大本营专业技术交流的和谐社区
  2. Springdata_自己的小小总结02
  3. 微软 Office Web Viewer 的使用
  4. 利用ADO.NET处理数据的简单之处
  5. 分清ul和li的边界
  6. java格式化货币_使用Java MessageFormat格式化货币
  7. yuv422 java_directdraw显示yuv422(yuy2)
  8. 普通夫妻 VS 程序员夫妻
  9. 黑桃k游戏java实战_JAVA入门第三季综合实战-简易扑克游戏
  10. 内向者沟通圣经:4P法(Preparation,Presence,Push,Practice)