锁的作用
锁的正确使用,可以保证多线程(本地多线程或者多机多节点多线程)情况下程序的效率及正确性。
效率:多线程不会做重复的工作。
正确性:多线程对同一数据的操作不会出现预期之外的结果。
保证共享资源数据的一致性,可以是同一时间控制一个或者几个线程才能访问或者修改共享资源。
本地程序中,锁的实现可以是JVM上一块资源即线程间共享的资源,多线程来竞争。如AQS中state;JUC 包下的同步类,lock,也可以java关键字来实现(synchronize)等。
在分布式系统中,不在同一jvm中,多机情况下则需要设置所有机器都能读取竞争的资源,如数据库,缓存,zk等第三方组件。
分布式锁需要实现哪些特性
-
互斥性(排他性):和本地锁一样互斥性是最基本,但是分布式锁需要保证在不同节点的不同线程的互斥。当然本地锁还有共享锁,即允许一定数量的线程同时并发执行
-
能防止死锁:加锁后,能在一定时间后能够释放,无论是程序正常释放还是程序异常后释放
-
高效,高可用:加锁和解锁需要高效,同时也需要保证高可用防止分布式锁失效。
-
支持阻塞和非阻塞(可选):和 ReentrantLock 一样支持 lock 和 trylock 以及 tryLock(long timeOut)。
-
支持公平锁和非公平锁(可选):公平锁的意思是按照请求加锁的顺序获得锁,非公平锁就相反是无序的。这个一般来说实现的比较少。
-
可重入性(可选):同一个节点上的同一个线程如果获取了锁之后那么也可以再次获取这个锁。
几种分布式锁的实现
一、基于数据库 MySql
1、加一个锁表
2、基于数据库悲观锁实现
3、基于数据库乐观锁实现
1)加一个锁表,基于数据库主键的唯一性或者唯一键的唯一性。设置某个需要加锁的方法为数据库字段的值,如果某个节点插入数据成功,则表示该节点获取到锁,可以执行该方法,伪代码如下:method_name 字段设置唯一索引
public boolean tryLock(String mothodName){
while(true){
try{
insertIntoDb(mothodName);
return true;
} catch(Exection e){
LockSupport.park();
}
}
}
释放锁则需要 delete 操作
该种实现方式的缺点:
-
严重依赖数据库,数据库挂了后获取不到锁(可以主备数据库)
-
锁没有失效时间,一旦释放锁异常,则后续所有节点都获取不到这个锁(可以定时任务定时清理这个表)
-
插入失败则直接异常,非阻塞操作不灵活
-
不可重入,即获取到锁的节点的线程也不能再次获取锁(可以加字段,记录机器信息及线程信息)
2)基于数据库悲观锁的实现。select * from t_lock for update; 伪代码如下:
public boolean tryLock(String mothodName){
connection.setAutoCommit(false);
while(true){
try{
result = select * from t_lock where method_name = methodName for update;
if(result){
return true;
}
} catch(Exection e){
}
LockSupport.park();
}
}
释放锁:connection.commit();
缺点:
-
单点数据库宕机问题
-
不可重入
-
如果不走索引,会表锁
-
commit之前占用数据库连接
3)基于乐观锁实现。 设置版本号或者某个字段根据状态更新.
select verison from table;
update table set verison = "version2" where version = "version1";
可在实际业务表中添加字段version,或者机器信息字段
基于数据库实现分布式锁的特点:
简单易实现
缺点:
对数据库依赖太大,很多服务瓶颈就在数据库,性能不高
需自我实现锁超时,事务等
基于缓存redis实现
基于redis 的 setNx (set if not exists)实现
public boolean tryLock(String key, String value){
Long result = jedis.setNx(key, value);
if(result != null && result == 1){
//设置失效时间
jedis.expire(key, seconds);
return true;
}
return false;
}
上述实现中,需要保证setNx 和 expire 操作的原子性。
Redis 2.8 版本之后,提供 set key value ex|px time nx
jedis实现如下:
/**
* Set the string value as value of the key. The string can't be longer than 1073741824 bytes (1
* GB).
* @param key
* @param value
* @param nxxx NX|XX, NX -- Only set the key if it does not already exist. XX -- Only set the key
* if it already exist.
* @param expx EX|PX, expire time units: EX = seconds; PX = milliseconds
* @param time expire time in the units of <code>expx</code>
* @return Status code reply
*/
public String set(final String key, final String value, final String nxxx, final String expx,
final long time) {
checkIsInMultiOrPipeline();
client.set(key, value, nxxx, expx, time);
return client.getStatusCodeReply();
}
释放锁:
public Long del(String key) { client.del(key); return client.getIntegerReply(); }
Redis 集群中,如果一个节点setNx 成功后,当前redis机器宕机了,没有同步数据到其他slave,那么其他节点在获取锁时依旧可以获取到。这时可以使用 RedLock 实现。
Redission 简单应用如下:
RedissonClient redissonClient = Redisson.create(config);
while (true) {
RLock lock = redissonClient.getLock("resource");
try {
//直接加锁
lock.tryLock();
//第一个参数代表等待时间,第二是代表超过时间释放锁,第三个代表设置的时间制单位
lock.tryLock(0, 1, TimeUnit.SECONDS);
System.out.println("do something");
} catch (InterruptedException e) {
e.printStackTrace();
break;
} finally {
//释放锁
lock.unlock();
}
}
集群使用:
RedissonClient client1 = Redisson.create(config);
RedissonClient client2 = Redisson.create(config);
RedissonClient client3 = Redisson.create(config);
RLock rLock1 = client1.getLock("lock1");
RLock rLock2 = client2.getLock("lock2");
RLock rLock3 = client3.getLock("lock3");
RedissonRedLock redissonRedLock = new RedissonRedLock(rLock1, rLock2, rLock3);
redissonRedLock.lock();
redissonRedLock.unlock();
redision 实现了Lock接口,使用时和其他锁类似
使用redis实现分布式锁的特点:
-
性能高,redis 的set 及 del 操作都很快
-
可以通过设置失效时间来防止死锁
-
实现简单,现成的api
缺点:
-
需要维护redis集群,redis 集群需要 redlock
zookeeper实现分布式锁
ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,具有通知事件机制的树形文件系统;结构如下:
zookeeper 特点
zookeeper 关键两个特性:
1、可以创建不同的节点,持久化节点-PERSISTENT,持久化有序节点-PERSISTENT_SEQUENTIAL,临时节点-EPHEMERAL,临时有序节点-EPHEMERAL_SEQUENTIAL。
创建临时节点,即便客户端宕机,失去连接后,该节点自动删除,不会发生死锁。
创建有序节点,可以认为获取最小的节点为获取到锁,这样保证公平以及同时只有一个客户端获取到锁
2、事件监听机制,client端会监听server端节点,当server端节点变化时会通知client。当前zookeeper有如下四种事件:1)节点创建;2)节点删除;3)节点数据修改;4)子节点变更
当锁释放后,即删除对应的最小的节点后,能够通知到其他客户端尝试比较获取锁
实现流程
结合上述两个特点,实现锁流程如下:
-
客户端连接zookeeper,并在/lock下创建临时的且有序的子节点,第一个客户端对应的子节点为/lock/lock-0000000000,第二个为/lock/lock-0000000001,以此类推。
-
客户端获取/lock下的子节点列表,判断自己创建的子节点是否为当前子节点列表中序号最小的子节点,如果是则认为获得锁,否则监听/lock的子节点变更消息,获得子节点变更通知后重复此步骤直至获得锁;
-
执行业务代码;
-
完成业务流程后,删除对应的子节点释放锁。
上述流程中,所有未获取到锁的子节点,均监听最小节点(即当前锁节点)事件,当锁释放后,zookeeper需要通知所有子节点客户端,这会阻塞其他的操作。其实除了第二小的节点之外,其他节点收到通知后也不能获取锁。
实现代码如下:
public boolean tryLock() {
try {
String splitStr = "_zklock_";
if (lockName.contains(splitStr)) {
throw new LockException("lockName can not contains _lock_");
}
//创建临时且有序的子节点
currentNode = zooKeeper.create(rootNode + "/" + lockName + splitStr, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
System.out.println(currentNode + " is created ");
//取出所有子节点
List<String> subNodes = zooKeeper.getChildren(rootNode, false);
//取出所有lockName的锁
List<String> lockObjNodes = new ArrayList<>();
for (String node : subNodes) {
String _node = node.split(splitStr)[0];
if (_node.equals(lockName)) {
lockObjNodes.add(node);
}
}
Collections.sort(lockObjNodes);
System.out.println(currentNode + "==" + lockObjNodes.get(0));
if (currentNode.equals(rootNode + "/" + lockObjNodes.get(0))) {
//如果是最小的节点,则表示取得锁
return true;
}
//如果不是最小的节点,找到比自己小1的节点
String subMyZnode = currentNode.substring(currentNode.lastIndexOf("/") + 1);
waitNode = lockObjNodes.get(Collections.binarySearch(lockObjNodes, subMyZnode) - 1);
} catch (KeeperException e) {
throw new LockException(e);
} catch (InterruptedException e) {
throw new LockException(e);
}
return false;
}
可以如上述一般使用zookeeper提供的API实现分布式锁的功能,也可以使用开源项目curator提供的基于zookeeper的实现,如下:
maven:
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.0.0</version>
</dependency>
使用:
//创建zookeeper的客户端
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
CuratorFramework client = CuratorFrameworkFactory.newClient("127.0.0.1:2181", retryPolicy);
client.start();
//创建分布式锁, 锁空间的根节点路径为/curator/lock
InterProcessMutex mutex = new InterProcessMutex(client, "/curator/lock");
mutex.acquire();
核心源码:
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();
//这里使用对象监视器做线程同步,当获取不到锁时监听前一个子节点删除消息并且进行wait(),当前一个子节点删除(也就是锁释放)时,回调会通过notifyAll唤醒此线程,此线程继续自旋判断是否获得锁
synchronized(this)
{
try
{
// use getData() instead of exists() to avoid leaving unneeded watchers which is a type of resource leak
client.getData().usingWatcher(watcher).forPath(previousSequencePath);
if ( millisToWait != null )
{
millisToWait -= (System.currentTimeMillis() - startMillis);
startMillis = System.currentTimeMillis();
if ( millisToWait <= 0 )
{
doDelete = true; // timed out - delete our node
break;
}
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;
}
参考: