1.前言

锁就像一把钥匙,需要加锁的代码就像一个房间。出现互斥操作的典型场景:多人同时想进同一个房间争抢这个房间的钥匙(只有一把),一人抢到钥匙,其他人都等待这个人出来归还钥匙,此时大家再次争抢钥匙循环下去。

作为终极实战系列,本篇用java语言分析锁的原理(源码剖析)和应用(详细代码),根据锁的作用范围分为:JVM锁和分布式锁。如理解有误之处,还请指出。

2.单JVM锁(进程级别)

程序部署在一台服务器上,当容器启动时(例如tomcat),一台JVM就运行起来了。本节分析的锁均只能在单JVM下生效。因为最终锁定的是某个对象,这个对象生存在JVM中,自然锁只能锁单JVM。这一点很重要。如果你的服务只部署一个实例,那么恭喜你,用以下几种锁就可以了。

1.synchronized同步锁

2.ReentrantLock重入锁

3.ReadWriteLock读写锁

4.StampedLock戳锁

由于之前已经详细分析过原理+使用,各位直接坐飞机吧:同步中的四种锁synchronized、ReentrantLock、ReadWriteLock、StampedLock

3.分布式锁(多服务节点,多进程)

3.1基于数据库锁实现

场景举例:

卖商品,先查询库存>0,更新库存-1。

1.悲观锁:select for update(一致性锁定读)


查询官方文档如上图,事务内起作用的行锁。能够保证当前session事务所锁定的行不会被其他session所修改(这里的修改指更新或者删除)。对读取的记录加X锁,即排它锁,其他事不能对上锁的行加任何锁。 BEGIN;(确保以下2步骤在一个事务中:)
SELECT * FROM tb_product_stock WHERE product_id=1 FOR UPDATE--->product_id有索引,锁行.加锁(注:条件字段必须有索引才能锁行,否则锁表,且最好用explain查看一下是否使用了索引,因为有一些会被优化掉最终没有使用索引)
UPDATE tb_product_stock SET number=number-1 WHERE product_id=1--->更新库存-1.解锁
COMMIT;

2.乐观锁:版本控制,选一个字段作为版本控制字段,更新前查询一次,更新时该字段作为更新条件。不同业务场景,版本控制字段,可以0 1控制,也可以+1控制,也可以-1控制,这个随意。

BEGIN;(确保以下2步骤在一个事务中:)
SELECT number FROM tb_product_stock WHERE product_id=1--》查询库存总数,不加锁
UPDATE tb_product_stock SET number=number-1 WHERE product_id=1 AND number=第一步查询到的库存数--》number字段作为版本控制字段
COMMIT; 

3.2基于缓存实现(redis,memcached)

原理:

redisson开源jar包,提供了很多功能,其中就包含分布式锁。是Redis官方推荐的顶级项目,官网飞机票

核心org.redisson.api.RLock接口封装了分布式锁的获取和释放。源码如下:

 @Override
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
long time = unit.toMillis(waitTime);
long current = System.currentTimeMillis();
final long threadId = Thread.currentThread().getId();
Long ttl = tryAcquire(leaseTime, unit, threadId);//申请锁,返回还剩余的锁过期时间
// lock acquired
if (ttl == null) {
return true;
} time -= (System.currentTimeMillis() - current);
if (time <= 0) {
acquireFailed(threadId);
return false;
} current = System.currentTimeMillis();
final RFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);
if (!await(subscribeFuture, time, TimeUnit.MILLISECONDS)) {
if (!subscribeFuture.cancel(false)) {
subscribeFuture.addListener(new FutureListener<RedissonLockEntry>() {
@Override
public void operationComplete(Future<RedissonLockEntry> future) throws Exception {
if (subscribeFuture.isSuccess()) {
unsubscribe(subscribeFuture, threadId);
}
}
});
}
acquireFailed(threadId);
return false;
} try {
time -= (System.currentTimeMillis() - current);
if (time <= 0) {
acquireFailed(threadId);
return false;
} while (true) {
long currentTime = System.currentTimeMillis();
ttl = tryAcquire(leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
return true;
} time -= (System.currentTimeMillis() - currentTime);
if (time <= 0) {
acquireFailed(threadId);
return false;
} // waiting for message
currentTime = System.currentTimeMillis();
if (ttl >= 0 && ttl < time) {
getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} else {
getEntry(threadId).getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
} time -= (System.currentTimeMillis() - currentTime);
if (time <= 0) {
acquireFailed(threadId);
return false;
}
}
} finally {
unsubscribe(subscribeFuture, threadId);
}
// return get(tryLockAsync(waitTime, leaseTime, unit));
}

上述方法,调用加锁的逻辑就是在tryAcquire(leaseTime, unit, threadId)中,如下图:

 private Long tryAcquire(long leaseTime, TimeUnit unit, long threadId) {
return get(tryAcquireAsync(leaseTime, unit, threadId));//tryAcquireAsync返回RFutrue
}
tryAcquireAsync中commandExecutor.evalWriteAsync就是咱们加锁核心方法了
 <T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
internalLockLeaseTime = unit.toMillis(leaseTime); return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('hset', 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.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}

如上图,已经到了redis命令了

加锁:

  • KEYS[1] :需要加锁的key,这里需要是字符串类型。
  • ARGV[1] :锁的超时时间,防止死锁
  • ARGV[2] :锁的唯一标识,(UUID.randomUUID()) + “:” + threadId
 // 检查是否key已经被占用,如果没有则设置超时时间和唯一标识,初始化value=1
if (redis.call('exists', KEYS[1]) == 0)
then
redis.call('hset', KEYS[1], ARGV[2], 1); //hset key field value 哈希数据结构
redis.call('pexpire', KEYS[1], ARGV[1]); //pexpire key expireTime 设置有效时间
return nil;
end;
// 如果锁重入,需要判断锁的key field 都一直情况下 value 加一
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1)
then
redis.call('hincrby', KEYS[1], ARGV[2], 1);//hincrby key filed addValue 加1
redis.call('pexpire', KEYS[1], ARGV[1]);//pexpire key expireTime重新设置超时时间
return nil;
end;
// 返回剩余的过期时间
return redis.call('pttl', KEYS[1]);

以上的方法,当返回空是,说明获取到锁,如果返回一个long数值(pttl 命令的返回值),说明锁已被占用,通过返回剩余时间,外部可以做一些等待时间的判断和调整。

不再分析解锁步骤,直接贴上解锁的redis 命令

解锁:

– KEYS[1] :需要加锁的key,这里需要是字符串类型。

– KEYS[2] :redis消息的ChannelName,一个分布式锁对应唯一的一个channelName:“redisson_lock__channel__{” + getName() + “}”

– ARGV[1] :reids消息体,这里只需要一个字节的标记就可以,主要标记redis的key已经解锁,再结合redis的Subscribe,能唤醒其他订阅解锁消息的客户端线程申请锁。

– ARGV[2] :锁的超时时间,防止死锁

– ARGV[3] :锁的唯一标识,(UUID.randomUUID()) + “:” + threadId

 // 如果key已经不存在,说明已经被解锁,直接发布(publihs)redis消息
if (redis.call('exists', KEYS[1]) == 0)
then
redis.call('publish', KEYS[2], ARGV[1]);//publish ChannelName message向信道发送解锁消息
return 1;
end;
// key和field不匹配,说明当前客户端线程没有持有锁,不能主动解锁。
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0)
then
return nil;
end;
// 将value减1
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); //hincrby key filed addValue 减1
// 如果counter>0说明锁在重入,不能删除key
if (counter > 0)
then
redis.call('pexpire', KEYS[1], ARGV[2]);
return 0;
else
// 删除key并且publish 解锁消息
redis.call('del', KEYS[1]);
redis.call('publish', KEYS[2], ARGV[1]);
return 1;
end;
return nil;

特点:

逻辑并不复杂, 实现了可重入功能, 通过pub/sub功能来减少空转,性能极高。

实现了Lock的大部分功能,支持强制解锁。

实战:

1.创建客户端配置类:

这里我们最终只用了一种来测试,就是initSingleServerConfig单例模式。

 package distributed.lock.redis;

 import org.redisson.config.Config;

 /**
*
* @ClassName:RedissionConfig
* @Description:自定义RedissionConfig初始化方法
* 支持自定义构造:单例模式,集群模式,主从模式,哨兵模式。
* 注:此处使用spring bean 配置文件保证bean单例,见applicationContext-redis.xml
* 大家也可以用工厂模式自己维护单例:本类生成RedissionConfig,再RedissonClient redisson = Redisson.create(config);这样就可以创建RedissonClient
* @author diandian.zhang
* @date 2017年7月20日下午12:55:50
*/
public class RedissionConfig {
private RedissionConfig() {
} public static Config initSingleServerConfig(String redisHost, String redisPort, String redisPassword) {
return initSingleServerConfig(redisHost, redisPort, redisPassword, 0);
} /**
*
* @Description 使用单例模式初始化构造Config
* @param redisHost
* @param redisPort
* @param redisPassword
* @param redisDatabase redis db 默认0 (0~15)有redis.conf配置文件中参数来控制数据库总数:database 16.
* @return
* @author diandian.zhang
* @date 2017年7月20日下午12:56:21
* @since JDK1.8
*/
public static Config initSingleServerConfig(String redisHost, String redisPort, String redisPassword,Integer redisDatabase) {
Config config = new Config();
config.useSingleServer().setAddress(redisHost + ":" + redisPort)
.setPassword(redisPassword)
.setDatabase(redisDatabase);//可以不设置,看业务是否需要隔离
//RedissonClient redisson = Redisson.create(config);
return config;
} /**
*
* @Description 集群模式
* @param masterAddress
* @param nodeAddressArray
* @return
* @author diandian.zhang
* @date 2017年7月20日下午3:29:32
* @since JDK1.8
*/
public static Config initClusterServerConfig(String masterAddress, String[] nodeAddressArray) {
String nodeStr = "";
for(String slave:nodeAddressArray){
nodeStr +=","+slave;
}
Config config = new Config();
config.useClusterServers()
.setScanInterval(2000) // cluster state scan interval in milliseconds
.addNodeAddress(nodeStr);
return config;
} /**
*
* @Description 主从模式
* @param masterAddress 一主
* @param slaveAddressArray 多从
* @return
* @author diandian.zhang
* @date 2017年7月20日下午2:29:38
* @since JDK1.8
*/
public static Config initMasterSlaveServerConfig(String masterAddress, String[] slaveAddressArray) {
String slaveStr = "";
for(String slave:slaveAddressArray){
slaveStr +=","+slave;
}
Config config = new Config();
config.useMasterSlaveServers()
.setMasterAddress(masterAddress)//一主
.addSlaveAddress(slaveStr);//多从"127.0.0.1:26389", "127.0.0.1:26379"
return config;
} /**
*
* @Description 哨兵模式
* @param masterAddress
* @param slaveAddressArray
* @return
* @author diandian.zhang
* @date 2017年7月20日下午3:01:35
* @since JDK1.8
*/
public static Config initSentinelServerConfig(String masterAddress, String[] sentinelAddressArray) {
String sentinelStr = "";
for(String sentinel:sentinelAddressArray){
sentinelStr +=","+sentinel;
}
Config config = new Config();
config.useSentinelServers()
.setMasterName("mymaster")
.addSentinelAddress(sentinelStr);
return config;
} }

2.分布式锁实现类

 package distributed.lock.redis;

 import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit; import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory; public class RedissonTest {
private static final Logger logger = LoggerFactory.getLogger(RedissonTest.class);
static SimpleDateFormat time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//这里可自定义多种模式,单例,集群,主从,哨兵模式。为了简单这里使用单例模式
private static RedissonClient redissonClient = Redisson.create(RedissionConfig.initSingleServerConfig("192.168.50.107", "6379", "password")); public static void main(String[] args) {
CountDownLatch latch = new CountDownLatch(3);
// key
String lockKey = "testkey20170802";
try {
Thread t1 = new Thread(() -> {
doWithLock(lockKey,latch);//函数式编程
}, "t1");
Thread t2 = new Thread(() -> {
doWithLock(lockKey,latch);
}, "t2");
Thread t3 = new Thread(() -> {
doWithLock(lockKey,latch);
}, "t3");
//启动线程
t1.start();
t2.start();
t3.start();
//等待全部完成
latch.await();
System.out.println("3个线程都解锁完毕,关闭客户端!");
redissonClient.shutdown();
} catch (Exception e) {
e.printStackTrace();
}
} /**
*
* @Description 线程执行函数体
* @param lockKey
* @author diandian.zhang
* @date 2017年8月2日下午3:37:32
* @since JDK1.8
*/
private static void doWithLock(String lockKey,CountDownLatch latch) {
try {
System.out.println("进入线程="+Thread.currentThread().getName()+":"+time.format(new Date()));
//获取锁,30秒内获取到返回true,未获取到返回false,60秒过后自动unLock
if (tryLock(lockKey, 30, 60, TimeUnit.SECONDS)) {
System.out.println(Thread.currentThread().getName() + " 获取锁成功!,执行需要加锁的任务"+time.format(new Date()));
Thread.sleep(2000L);//休息2秒模拟执行需要加锁的任务
//获取锁超时
}else{
System.out.println(Thread.currentThread().getName() + " 获取锁超时!"+time.format(new Date()));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
//释放锁
unLock(lockKey);
latch.countDown();//完成,计数器减一
} catch (Exception e) {
e.printStackTrace();
}
}
} /**
*
* @Description 获取锁,锁waitTime时间内获取到返回true,未获取到返回false,租赁期leaseTime过后unLock(除非手动释放锁)
* @param key
* @param waitTime
* @param leaseTime
* @param timeUnit
* @return
* @author diandian.zhang
* @date 2017年8月2日下午3:24:09
* @since JDK1.8
*/
public static boolean tryLock(String key, long waitTime, long leaseTime, TimeUnit timeUnit) {
try {
//根据key获取锁实例,非公平锁
RLock lock = redissonClient.getLock(key);
//在leaseTime时间内阻塞获取锁,获取锁后持有锁直到leaseTime租期结束(除非手动unLock释放锁)。
return lock.tryLock(waitTime, leaseTime, timeUnit);
} catch (Exception e) {
logger.error("redis获取分布式锁异常;key=" + key + ",waitTime=" + waitTime + ",leaseTime=" + leaseTime +
",timeUnit=" + timeUnit, e);
return false;
}
} /**
*
* @Description 释放锁
* @param key
* @author diandian.zhang
* @date 2017年8月2日下午3:25:34
* @since JDK1.8
*/
public static void unLock(String key) {
RLock lock = redissonClient.getLock(key);
lock.unlock();
System.out.println(Thread.currentThread().getName() + " 释放锁"+time.format(new Date()));
}
}

执行结果如下:

 进入线程=t3:2017-08-02 16:33:19
进入线程=t1:2017-08-02 16:33:19
进入线程=t2:2017-08-02 16:33:19
t2 获取锁成功!,执行需要加锁的任务2017-08-02 16:33:19--->T2 19秒时获取到锁
t2 释放锁2017-08-02 16:33:21--->T2任务完成,21秒时释放锁
t1 获取锁成功!,执行需要加锁的任务2017-08-02 16:33:21--->T1 21秒时获取到锁
t1 释放锁2017-08-02 16:33:23--->T2任务完成,23秒时释放锁
t3 获取锁成功!,执行需要加锁的任务2017-08-02 16:33:23--->T3 23秒时获取到锁
t3 释放锁2017-08-02 16:33:25--->T2任务完成,25秒时释放锁
3个线程都解锁完毕,关闭客户端!

如上图,3个线程共消耗25-19=6秒,验证通过,确实互斥锁住了。

我们用Redis Desktop Manger来看一下redis中数据:

 192.168.50.107:0>hgetall "testkey20170802"--->用key查询hash所有的值
1) 159b46b3-8bc5-4447-ad57-c55fdd381384:30--->T2获取到锁field=uuid:线程号
2) 1 --->value=1代表重入次数为1
192.168.50.107:0>hgetall "testkey20170802"--->T2释放锁,T1获取到锁
1) 159b46b3-8bc5-4447-ad57-c55fdd381384:29
2) 1
192.168.50.107:0>hgetall "testkey20170802"--->T1释放锁,T3获取到锁
1) 159b46b3-8bc5-4447-ad57-c55fdd381384:31
2) 1
192.168.50.107:0>hgetall "testkey20170802"--->最后一次查询时,T3释放锁,已无数据

3.3基于zookeeper实现

原理:

每个客户端(每个JVM内部共用一个客户端实例)对某个方法加锁时,在zookeeper上指定节点的目录下,生成一个唯一的瞬时有序节点。判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。当释放锁的时候,只需将这个瞬时节点删除即可。

我们使用apache的Curator组件来实现,一般使用Client、Framework、Recipes三个组件。

curator下,InterProcessMutex可重入互斥公平锁,源码(curator-recipes-2.4.1.jar)注释如下:

A re-entrant mutex that works across JVMs. Uses Zookeeper to hold the lock. All processes in all JVMs that use the same lock path will achieve an inter-process critical section. Further, this mutex is "fair" - each user will get the mutex in the order requested (from ZK's point of view)

即一个在JVM上工作的可重入互斥锁。使用ZK去持有这把锁。在所有JVM中的进程组,只要使用相同的锁路径将会获得进程间的临界资源。进一步说,这个互斥锁是公平的-因为每个线程将会根据请求顺序获得这个互斥量(对于ZK来说)

主要方法如下:

     // 构造方法
public InterProcessMutex(CuratorFramework client, String path)
public InterProcessMutex(CuratorFramework client, String path, LockInternalsDriver driver)
// 通过acquire获得锁,并提供超时机制:
public void acquire() throws Exception
public boolean acquire(long time, TimeUnit unit) throws Exception
// 撤销锁
public void makeRevocable(RevocationListener<InterProcessMutex> listener)
public void makeRevocable(final RevocationListener<InterProcessMutex> listener, Executor executor)

我们主要分析核心获取锁acquire方法如下:

 @Override
public boolean acquire(long time, TimeUnit unit) throws Exception
{
return internalLock(time, unit);
} private boolean internalLock(long time, TimeUnit unit) throws Exception
{
/*
Note on concurrency: a given lockData instance
can be only acted on by a single thread so locking isn't necessary
*/ Thread currentThread = Thread.currentThread();
//线程安全map:private final ConcurrentMap<Thread, LockData>   threadData = Maps.newConcurrentMap();
LockData lockData = threadData.get(currentThread);
if ( lockData != null )
{
//这里实现了可重入,如果当前线程已经获取锁,计数+1,直接返回true
lockData.lockCount.incrementAndGet();
return true;
}
//获取锁,核心方法
String lockPath = internals.attemptLock(time, unit, getLockNodeBytes());
if ( lockPath != null )
{ //得到锁,塞进线程安全map
LockData newLockData = new LockData(currentThread, lockPath);
threadData.put(currentThread, newLockData);
return true;
} return false;
}

核心获取锁的方法attemptLock源码如下:

 String attemptLock(long time, TimeUnit unit, byte[] lockNodeBytes) throws Exception
{
final long startMillis = System.currentTimeMillis();
final Long millisToWait = (unit != null) ? unit.toMillis(time) : null;
final byte[] localLockNodeBytes = (revocable.get() != null) ? new byte[0] : lockNodeBytes;
int retryCount = 0; String ourPath = null;
boolean hasTheLock = false;
boolean isDone = false;
while ( !isDone )
{
isDone = true; try
{
if ( localLockNodeBytes != null )
{
ourPath = client.create().creatingParentsIfNeeded().withProtection().withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath(path, localLockNodeBytes);
}
else
{ //创建瞬时节点(客户端断开连接时删除),节点名追加自增数字
ourPath = client.create().creatingParentsIfNeeded().withProtection().withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath(path);
}
//自循环等待时间,并判断是否获取到锁
hasTheLock = internalLockLoop(startMillis, millisToWait, ourPath);
}
catch ( KeeperException.NoNodeException e )
{
// gets thrown by StandardLockInternalsDriver when it can't find the lock node
// this can happen when the session expires, etc. So, if the retry allows, just try it all again
if ( client.getZookeeperClient().getRetryPolicy().allowRetry(retryCount++, System.currentTimeMillis() - startMillis, RetryLoop.getDefaultRetrySleeper()) )
{
isDone = false;
}
else
{
throw e;
}
}
}
//获取到锁返回节点path
if ( hasTheLock )
{
return ourPath;
} return null;
}
自循环等待时间:
  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); // 获取当前节点名称
16 //核心方法:判断是否获取到锁
PredicateResults predicateResults = driver.getsTheLock(client, children, sequenceNodeName, maxLeases);
if ( predicateResults.getsTheLock() )//获取到锁,置true,下一次循环退出
{
haveTheLock = true;
}
else//没有索取到锁
{
String previousSequencePath = basePath + "/" + predicateResults.getPathToWatch();//这里路径是上一次获取到锁的持有锁路径 synchronized(this)//强制加锁
{
                 //让线程等待,并且watcher当前节点,当节点有变化的之后,则notifyAll当前等待的线程,让它再次进入来争抢锁
Stat stat = client.checkExists().usingWatcher(watcher).forPath(previousSequencePath);
if ( stat != null )
{
if ( millisToWait != null )
{
millisToWait -= (System.currentTimeMillis() - startMillis);
startMillis = System.currentTimeMillis();
if ( millisToWait <= 0 )
{
doDelete = true; //等待超时,置状态为true,后面会删除节点
break;
}
//等待指定时间
wait(millisToWait);
}
else
{ //一直等待
wait();
}
}
}
// else it may have been deleted (i.e. lock released). Try to acquire again
}
}
}
catch ( Exception e )
{
doDelete = true;
throw e;
}
finally
{
if ( doDelete )//删除path
{
deleteOurPath(ourPath);
}
}
return haveTheLock;
}
 @Override
public PredicateResults getsTheLock(CuratorFramework client, List<String> children, String sequenceNodeName, int maxLeases) throws Exception
{
int ourIndex = children.indexOf(sequenceNodeName);//先根据子节点名获取children(所有子节点升序集合)中的索引
validateOurIndex(sequenceNodeName, ourIndex);//校验如果索引为负值,即不存在该子节点
//maxLeases允许同时租赁的数量,这里源代码写死了1,但这种设计符合将来拓展,修改maxLeases即可满足多租赁
boolean getsTheLock = ourIndex < maxLeases;//maxLeases=1,所以只有当index=0时才是true,即所有子节点中升序排序第一个最小值,即第一个请求过来的,这就是核心思想所在!
String pathToWatch = getsTheLock ? null : children.get(ourIndex - maxLeases);//获取到锁返回null,否则未获取到锁,获取上一次的获取到锁的路径。后面会监视这个路径用以唤醒请求线程 return new PredicateResults(pathToWatch, getsTheLock);
}

特点:

1.可避免死锁:zk瞬时节点(Ephemeral Nodes)生命周期和session一致,session结束,节点自动删除。
2.依赖zk创建节点,涉及文件操作,开销较大。

实战:

1.创建客户端client
2.生成互斥锁InterProcessMutex
3.开启3个线程去获取锁

 package distributed.lock.zk;

 import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit; import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.curator.retry.RetryNTimes;
import org.jboss.netty.channel.StaticChannelPipeline;
import org.omg.CORBA.PRIVATE_MEMBER; /**
*
* @ClassName:CuratorDistrLockTest
* @Description:Curator包实现zk分布式锁:利用了zookeeper的临时顺序节点特性,一旦客户端失去连接后,则就会自动清除该节点。
* @author diandian.zhang
* @date 2017年7月11日下午12:43:44
*/ public class CuratorDistrLock {
private static final String ZK_ADDRESS = "192.168.50.253:2181";//zk
private static final String ZK_LOCK_PATH = "/zktest";//path
static SimpleDateFormat time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public static void main(String[] args) {
try {
//创建zk客户端
// CuratorFramework client = CuratorFrameworkFactory.newClient(ZK_ADDRESS,new RetryNTimes(3, 1000));
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString(ZK_ADDRESS)
.sessionTimeoutMs(5000)
.retryPolicy(new ExponentialBackoffRetry(1000, 10))
.build();
//开启
client.start();
System.out.println("zk client start successfully!"+time.format(new Date())); Thread t1 = new Thread(() -> {
doWithLock(client);//函数式编程
}, "t1");
Thread t2 = new Thread(() -> {
doWithLock(client);
}, "t2");
Thread t3 = new Thread(() -> {
doWithLock(client);
}, "t3");
//启动线程
t1.start();
t2.start();
t3.start();
} catch (Exception e) {
e.printStackTrace();
}
} /**
*
* @Description 线程执行函数体
* @param client
* @param lock
* @author diandian.zhang
* @date 2017年7月12日下午6:00:53
* @since JDK1.8
*/
private static void doWithLock(CuratorFramework client) {
//依赖ZK生成的可重入互斥公平锁(按照请求顺序)
InterProcessMutex lock = new InterProcessMutex(client, ZK_LOCK_PATH);
try {
System.out.println("进入线程="+Thread.currentThread().getName()+":"+time.format(new Date())); //花20秒时间尝试获取锁
if (lock.acquire(20, TimeUnit.SECONDS)) {
System.out.println(Thread.currentThread().getName() + " 获取锁成功!,执行需要加锁的任务"+time.format(new Date()));
Thread.sleep(2000L);//休息2秒模拟执行需要加锁的任务
//获取锁超时
}else{
System.out.println(Thread.currentThread().getName() + " 获取锁超时!"+time.format(new Date()));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
//当前线程获取到锁,那么最后需要释放锁(实际上是删除节点)
if (lock.isAcquiredInThisProcess()) {
lock.release();
System.out.println(Thread.currentThread().getName() + " 释放锁"+time.format(new Date()));
}
} catch (Exception e) {
e.printStackTrace();
}
}
} }

执行结果:

zk client start successfully!
进入线程=t2:-- ::
进入线程=t1:-- ::
进入线程=t3:-- ::
t2 获取锁成功!,执行需要加锁的任务2017-- ::23----》起始时间23秒
t2 释放锁2017-- ::
t3 获取锁成功!,执行需要加锁的任务2017-- ::25----》验证耗时2秒,T2执行完,T3执行
t3 释放锁2017-- ::
t1 获取锁成功!,执行需要加锁的任务2017-- ::27----》验证耗时2秒,T3执行完,T1执行
t1 释放锁2017-- ::29----》验证耗时2秒,T1执行完,3个任务共耗时=29-23=6秒,验证互斥锁达到目标。

查看zookeeper节点

1.客户端连接

zkCli.sh -server 192.168.50.253:2181

2.查看节点

[zk: 192.168.50.253:2181(CONNECTED) 80] ls /-----》查看根目录
[dubbo, zktest, zookeeper, test]

[zk: 192.168.50.253:2181(CONNECTED) 81] ls /zktest -----》查看我们创建的子节点
[_c_034e5f23-abaf-4d4a-856f-c27956db574e-lock-0000000007, _c_63c708f1-2c3c-4e59-9d5b-f0c70c149758-lock-0000000006, _c_1f688cb7-c38c-4ebb-8909-0ba421e484a4-lock-0000000008]

[zk: 192.168.50.253:2181(CONNECTED) 82] ls /zktest-----》任务执行完毕最终释放了子节点
[]

4.总结比较

一级锁分类

二级锁分类

锁名称

特性

是否推荐

单JVM锁

基于JVM源生synchronized关键字实现

synchronized同步锁

 适用于低并发的情况,性能稳定。 新手推荐
基于JDK实现,需显示获取锁,释放锁

ReentrantLock可重入锁

 适用于低、高并发的情况,性能较高  需要指定公平、非公平或condition时使用。

ReentrantReadWriteLock

可重入读写锁

 适用于读多写少的情况。性能高。  老司机推荐

StampedLock戳锁

 JDK8才有,适用于高并发且读远大于写时,支持乐观读,票据校验失败后可升级悲观读锁,性能极高!  老司机推荐

分布式锁

基于数据库锁实现

悲观锁:select for update

 sql直接使用,但水很深。设计数据库ACID原理+隔离级别+不同数据库规范  高端老司机推荐

乐观锁:版本控制

 自己实现字段版本控制  新手推荐

基于缓存实现

org.redisson

 性能极高,支持除了分布式锁外还实现了分布式对象、分布式集合等极端强大的功能  老司机推荐

基于zookeeper实现

org.apache.curator zookeeper

 性能较高,除支持分布式锁外,还实现了master选举、节点监听()、分布式队列、Barrier、AtomicLong等计数器  老司机推荐

=====附Redis命令=======

  1. SETNX key value (SET if Not eXists):当且仅当 key 不存在,将 key 的值设为 value ,并返回1;若给定的 key 已经存在,则 SETNX 不做任何动作,并返回0。详见:SETNX commond
  2. GETSET key value:将给定 key 的值设为 value ,并返回 key 的旧值 (old value),当 key 存在但不是字符串类型时,返回一个错误,当key不存在时,返回nil。详见:GETSET commond
  3. GET key:返回 key 所关联的字符串值,如果 key 不存在那么返回 nil 。详见:GET Commond
  4. DEL key [KEY …]:删除给定的一个或多个 key ,不存在的 key 会被忽略,返回实际删除的key的个数(integer)。详见:DEL Commond
  5. HSET key field value:给一个key 设置一个{field=value}的组合值,如果key没有就直接赋值并返回1,如果field已有,那么就更新value的值,并返回0.详见:HSET Commond
  6. HEXISTS key field:当key 中存储着field的时候返回1,如果key或者field至少有一个不存在返回0。详见HEXISTS Commond
  7. HINCRBY key field increment:将存储在 key 中的哈希(Hash)对象中的指定字段 field 的值加上增量 increment。如果键 key 不存在,一个保存了哈希对象的新建将被创建。如果字段 field 不存在,在进行当前操作前,其将被创建,且对应的值被置为 0。返回值是增量之后的值。详见:HINCRBY Commond
  8. PEXPIRE key milliseconds:设置存活时间,单位是毫秒。expire操作单位是秒。详见:PEXPIRE Commond
  9. PUBLISH channel message:向channel post一个message内容的消息,返回接收消息的客户端数。详见PUBLISH Commond

======参考======

分布式锁的几种实现方式~

基于Redis实现分布式锁,Redisson使用及源码分析

终极锁实战:单JVM锁+分布式锁的更多相关文章

  1. 使用ZooKeeper实现Java跨JVM的分布式锁(读写锁)

    一.使用ZooKeeper实现Java跨JVM的分布式锁 二.使用ZooKeeper实现Java跨JVM的分布式锁(优化构思) 三.使用ZooKeeper实现Java跨JVM的分布式锁(读写锁) 读写 ...

  2. 使用ZooKeeper实现Java跨JVM的分布式锁(优化构思)

    一.使用ZooKeeper实现Java跨JVM的分布式锁 二.使用ZooKeeper实现Java跨JVM的分布式锁(优化构思) 三.使用ZooKeeper实现Java跨JVM的分布式锁(读写锁) 说明 ...

  3. 使用ZooKeeper实现Java跨JVM的分布式锁

    一.使用ZooKeeper实现Java跨JVM的分布式锁 二.使用ZooKeeper实现Java跨JVM的分布式锁(优化构思) 三.使用ZooKeeper实现Java跨JVM的分布式锁(读写锁) 说明 ...

  4. 单实例redis分布式锁的简单实现

    redis分布式锁的基本功能包括, 同一刻只能有一个人占有锁, 当锁被其他人占用时, 获取者可以等待他人释放锁, 此外锁本身必须能超时自动释放. 直接上java代码, 如下: package com. ...

  5. 图解Janusgraph系列-并发安全:锁机制(本地锁+分布式锁)分析

    图解Janusgraph系列-并发安全:锁机制(本地锁+分布式锁)分析 大家好,我是洋仔,JanusGraph图解系列文章,实时更新~ 图数据库文章总目录: 整理所有图相关文章,请移步(超链):图数据 ...

  6. Spring Boot 2实现分布式锁——这才是实现分布式锁的正确姿势!

    参考资料 网址 Spring Boot 2实现分布式锁--这才是实现分布式锁的正确姿势! http://www.spring4all.com/article/6892

  7. SpringBoot--防止重复提交(锁机制---本地锁、分布式锁)

    防止重复提交,主要是使用锁的形式来处理,如果是单机部署,可以使用本地缓存锁(Guava)即可,如果是分布式部署,则需要使用分布式锁(可以使用zk分布式锁或者redis分布式锁),本文的分布式锁以red ...

  8. Spring Boot + Redis实战-利用自定义注解+分布式锁实现接口幂等性

    场景 不管是传统行业还是互联网行业,我们都需要保证大部分操作是幂等性的,简单点说,就是无论用户点击多少次,操作多少遍,产生的结果都是一样的,是唯一的.而今次公司的项目里,又被我遇到了这么一个幂等性的问 ...

  9. 分布式锁(一) Zookeeper分布式锁

    什么是Zookeeper? Zookeeper(业界简称zk)是一种提供配置管理.分布式协同以及命名的中心化服务,这些提供的功能都是分布式系统中非常底层且必不可少的基本功能,但是如果自己实现这些功能而 ...

  10. 分布式锁之三:Redlock实现分布式锁

    之前写过一篇文章<如何在springcloud分布式系统中实现分布式锁?>,由于自己仅仅是阅读了相关的书籍,和查阅了相关的资料,就认为那样的是可行的.那篇文章实现的大概思路是用setNx命 ...

随机推荐

  1. Web压力测试软件webbench

    官方网站:http://home.tiscali.cz/~cz210552/webbench.html下载地址:http://home.tiscali.cz/~cz210552/distfiles/w ...

  2. WPF Dashboard仪表盘控件的实现

    1.确定控件应该继承的基类 从表面上看,目前WPF自带常用控件中,没有一个是接近这个表盘控件的,但将该控件拆分就能够发现,该控件的每个子部分都是在WPF中存在的,因此我们需要将各个子控件组合才能形成这 ...

  3. Cordova各个插件使用介绍系列(六)—$cordovaDevice获取设备的相关信息

    详情请看:Cordova各个插件使用介绍系列(六)—$cordovaDevice获取设备的相关信息 在项目中需要获取到当前设备,例如手机的ID,联网状态,等,然后这个Cordova里有这个插件可以用, ...

  4. Lucence_Curd

    设置Field的类型 new StringField 不分词(id,身份证号,电话...) new StoredField 不分词(链接) new TextField 分词(文本) new Fload ...

  5. 关于MATLAB处理大数据坐标文件201761

    前几天备战考试,接下来的日子将会继续攻克大数据比赛 虽然停止了一段时间没有提交数据,但是这几天的收获还是有的,对Python 随机森林了解的更了解了 随机森林是由多课决策树组成(当然这个虽然我们初学者 ...

  6. scanner--inputstreamreader--console对比

    1 JDK 1.4 及以下版本读取的方法 JDK 1.4 及以下的版本中要想从控制台中输入数据只有一种办法,即使用System.in获得系统的输入流,再桥接至字符流从字符流中读入数据.示例代码如下: ...

  7. tcpdf导出pdf数据支持中文的解决方案

    步骤如下:1.确保你测试tcpdf能正常输出英文内容的pdf2.测试输入中文内容后显示是?的乱码或者空白分析原因,是因为我们输入的中文,tcpdf字体库并不支持,因此乱码或者空白显示 添加一个合适的字 ...

  8. php中curl远程调用获取数据

    $jump_url=$this->_post('locations'); $url=htmlspecialchars_decode($jump_url); $ch = curl_init(); ...

  9. tkinter模块常用参数(python3)

    1.使用tkinter.Tk() 生成主窗口(root=tkinter.Tk()):root.title('标题名')    修改框体的名字,也可在创建时使用className参数来命名:root.r ...

  10. 水题 第三站 HDU Largest prime factor

    先写一遍思路,跟素数表很类似吧. 1)从小到大遍历数据范围内的所有数.把包含质因子的数的位置都设成跟质因子的位置相同. 2)同一个数的位置可能被多次复写.但是由于是从小到大遍历,这就保证了最后一次写入 ...