1. 什么是分布式锁
一般的锁:一般我们说的锁是单进程多线程的锁,在多线程并发编程中,用于线程之间的数据同步,保护共享资源的访问
分布式锁:分布式锁指的是在分布式环境下,保护跨进程,跨主机,跨网络的共享资源,实现互斥访问,保证一致性。
2. 应用场景介绍
场景1:
场景2:
某服务提供一组任务,A请求随机从任务组中获取一个任务;B请求随机从任务组中获取一个任务。
在理想的情况下,A从任务组中挑选一个任务,任务组删除该任务,B从剩下的的任务中再挑一个,任务组删除该任务。
同样的,在真实情况下,如果不做任何处理,可能会出现A和B挑中了同一个任务的情况。
3. 我们需要的分布式锁应该是怎么样的?
1.可以保证在分布式部署的应用集群中,同一个方法在同一时间只能被一台机器上的一个线程执行。
2.这把锁要是一把可重入锁(避免死锁)
3.这把锁最好是一把阻塞锁(根据业务需求考虑要不要这条)
4.有高可用的获取锁和释放锁功能
5.获取锁和释放锁的性能要好
基于数据库的分布式锁
最简单的方式就是直接创建一张锁表,加上唯一索引,然后通过操作该表中的数据来实现了。
当我们要锁住某个方法或资源时,我们就在该表中增加一条记录,想要释放锁的时候就删除这条记录。
CREATE TABLE `methodLock` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`method_name` varchar(64) NOT NULL DEFAULT '' COMMENT '锁定的方法名',
`desc` varchar(1024) NOT NULL DEFAULT '备注信息',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '保存数据时间,自动生成',
PRIMARY KEY (`id`),
UNIQUE KEY `uidx_method_name` (`method_name `) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='锁定中的方法';
当我们想要锁住某个方法时,执行以下SQL:
insert into methodLock(method_name,desc) values (‘method_name’,‘desc’);
因为我们对method_name做了唯一性约束,这里如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就可以认为操作成功的那个线程获得了该方法的锁,可以执行方法体内容。
当方法执行完毕之后,想要释放锁的话,需要执行以下Sql:
delete from methodLock where method_name ='method_name';
上面这种简单的实现有以下几个问题:
1、这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。
2、这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。
3、这把锁只能是非阻塞的,因为数据的insert操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作。
4、这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。
当然,我们也可以有其他方式解决上面的问题。
• 数据库是单点?搞两个数据库,数据之前双向同步。一旦挂掉快速切换到备库上。
• 没有失效时间?只要做一个定时任务,每隔一定时间把数据库中的超时数据清理一遍。
• 非阻塞的?搞一个while循环,直到insert成功再返回成功。
• 非重入的?在数据库表中加个字段,记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了。
基于数据库排他锁
除了可以通过增删操作数据表中的记录以外,其实还可以借助数据中自带的锁来实现分布式的锁。
我们还用刚刚创建的那张数据库表。可以通过数据库的排他锁来实现分布式锁。
基于MySQL的InnoDB引擎,可以使用以下方法来实现加锁操作:
public boolean lock(){
connection.setAutoCommit(false)
while(true){
try{
result = select * from methodLock where method_name=xxx for update;
if(result==null){
return true;
}
}catch(Exception e){
}
sleep(1000);
}
return false;
}
在查询语句后面增加for update,数据库会在查询过程中给数据库表增加排他锁。当某条记录被加上排他锁之后,其他线程无法再在该行记录上增加排他锁。
我们可以认为获得排它锁的线程即可获得分布式锁,当获取到锁之后,可以执行方法的业务逻辑,执行完方法之后,再通过以下方法解锁:
public void unlock(){
connection.commit();
}
通过connection.commit()操作来释放锁。
这种方法可以有效的解决上面提到的无法释放锁和阻塞锁的问题。
• 阻塞锁? for update语句会在执行成功后立即返回,在执行失败时一直处于阻塞状态,直到成功。
• 锁定之后服务宕机,无法释放?使用这种方式,服务宕机之后数据库会自己把锁释放掉。
但是还是无法直接解决数据库单点和可重入问题。
总结
总结一下使用数据库来实现分布式锁的方式,这两种方式都是依赖数据库的一张表,一种是通过表中的记录的存在情况确定当前是否有锁存在,另外一种是通过数据库的排他锁来实现分布式锁。
数据库实现分布式锁的优点
直接借助数据库,容易理解。
数据库实现分布式锁的缺点
会有各种各样的问题,在解决问题的过程中会使整个方案变得越来越复杂。
操作数据库需要一定的开销,性能问题需要考虑。
基于redis缓存实现分布式锁
基于zookeeper的分布式锁
1. 分布式锁的架构图
左侧是zookeeper集群,locker是数据节点,node_1到node_n代表一系列的顺序节点。
右侧client_1至client_n代表客户端,Service代表需要互斥访问的服务。
总实现思路,是在获取锁的时候在locker节点下创建顺序节点,在释放锁的时候,把自己创建的节点删除。
2. 分布式锁的算法流程
3. 代码如下
package com.dsk.modules.xcx.test;
import java.util.concurrent.Callable;
import org.I0Itec.zkclient.ZkClient;
import org.I0Itec.zkclient.serialize.ZkSerializer;
import org.apache.zookeeper.data.Stat;
/**
* @ClassName: ZkClientExt.java
* @Description: zk客户端方法重写
* @version: v1.0.0
* @author: gaocj
* @date: 2018年10月18日 上午10:18:18
*/
public class ZkClientExt extends ZkClient {
public ZkClientExt(String zkServers, int sessionTimeout, int connectionTimeout, ZkSerializer zkSerializer) {
super(zkServers, sessionTimeout, connectionTimeout, zkSerializer);
}
@Override
public void watchForData(final String path) {
retryUntilConnected(new Callable<Object>() {
public Object call() throws Exception {
Stat stat = new Stat();
_connection.readData(path, stat, true);
return null;
}
});
}
}
package com.dsk.modules.xcx.test;
import java.util.concurrent.TimeUnit;
/**
* @ClassName: DistributedLock.java
* @Description: 分布式锁接口
* @version: v1.0.0
* @author: gaocj
* @date: 2018年10月18日 上午10:07:46
*/
public interface DistributedLock {
/*
* 获取锁,如果没有得到就等待
*/
public void acquire() throws Exception;
/*
* 获取锁,直到超时
*/
public boolean acquire(long time, TimeUnit unit) throws Exception;
/*
* 释放锁
*/
public void release() throws Exception;
}
package com.dsk.modules.xcx.test;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.I0Itec.zkclient.IZkDataListener;
import org.I0Itec.zkclient.ZkClient;
import org.I0Itec.zkclient.exception.ZkNoNodeException;
public class BaseDistributedLock {
private final ZkClientExt client;
private final String path;
private final String basePath;
private final String lockName;
private static final Integer MAX_RETRY_COUNT = 10;
public BaseDistributedLock(ZkClientExt client, String path, String lockName){
this.client = client;
this.basePath = path;
this.path = path.concat("/").concat(lockName);
this.lockName = lockName;
}
// 删除成功获取锁之后所创建的那个顺序节点
private void deleteOurPath(String ourPath) throws Exception{
client.delete(ourPath);
}
// 创建临时顺序节点
private String createLockNode(ZkClient client, String path) throws Exception{
return client.createEphemeralSequential(path, null);
}
// 等待比自己次小的顺序节点的删除
private boolean waitToLock(long startMillis, Long millisToWait, String ourPath) throws Exception{
boolean haveTheLock = false;
boolean doDelete = false;
try {
while ( !haveTheLock ) {
// 获取/locker下的经过排序的子节点列表
List<String> children = getSortedChildren();
// 获取刚才自己创建的那个顺序节点名
String sequenceNodeName = ourPath.substring(basePath.length()+1);
// 判断自己排第几个
int ourIndex = children.indexOf(sequenceNodeName);
if (ourIndex < 0){ // 网络抖动,获取到的子节点列表里可能已经没有自己了
throw new ZkNoNodeException("节点没有找到: " + sequenceNodeName);
}
// 如果是第一个,代表自己已经获得了锁
boolean isGetTheLock = ourIndex == 0;
// 如果自己没有获得锁,则要watch比我们次小的那个节点
String pathToWatch = isGetTheLock ? null : children.get(ourIndex - 1);
if ( isGetTheLock ){
haveTheLock = true;
} else {
// 订阅比自己次小顺序节点的删除事件
String previousSequencePath = basePath .concat( "/" ) .concat( pathToWatch );
final CountDownLatch latch = new CountDownLatch(1);
final IZkDataListener previousListener = new IZkDataListener() {
public void handleDataDeleted(String dataPath) throws Exception {
latch.countDown(); // 删除后结束latch上的await
}
public void handleDataChange(String dataPath, Object data) throws Exception {
// ignore
}
};
try {
//订阅次小顺序节点的删除事件,如果节点不存在会出现异常
client.subscribeDataChanges(previousSequencePath, previousListener);
if ( millisToWait != null ) {
millisToWait -= (System.currentTimeMillis() - startMillis);
startMillis = System.currentTimeMillis();
if ( millisToWait <= 0 ) {
doDelete = true; // timed out - delete our node
break;
}
latch.await(millisToWait, TimeUnit.MICROSECONDS); // 在latch上await
} else {
latch.await(); // 在latch上await
}
// 结束latch上的等待后,继续while重新来过判断自己是否第一个顺序节点
}
catch ( ZkNoNodeException e ) {
//ignore
} finally {
client.unsubscribeDataChanges(previousSequencePath, previousListener);
}
}
}
}
catch ( Exception e ) {
//发生异常需要删除节点
doDelete = true;
throw e;
} finally {
//如果需要删除节点
if ( doDelete ) {
deleteOurPath(ourPath);
}
}
return haveTheLock;
}
private String getLockNodeNumber(String str, String lockName) {
int index = str.lastIndexOf(lockName);
if ( index >= 0 ) {
index += lockName.length();
return index <= str.length() ? str.substring(index) : "";
}
return str;
}
// 获取/locker下的经过排序的子节点列表
List<String> getSortedChildren() throws Exception {
try{
List<String> children = client.getChildren(basePath);
Collections.sort(
children, new Comparator<String>() {
public int compare(String lhs, String rhs) {
return getLockNodeNumber(lhs, lockName).compareTo(getLockNodeNumber(rhs, lockName));
}
}
);
return children;
} catch (ZkNoNodeException e){
client.createPersistent(basePath, true);
return getSortedChildren();
}
}
protected void releaseLock(String lockPath) throws Exception{
deleteOurPath(lockPath);
System.out.println("释放锁==="+lockPath);
}
protected String attemptLock(long time, TimeUnit unit) throws Exception {
final long startMillis = System.currentTimeMillis();
final Long millisToWait = (unit != null) ? unit.toMillis(time) : null;
String ourPath = null;
boolean hasTheLock = false;
boolean isDone = false;
int retryCount = 0;
//网络闪断需要重试一试
while ( !isDone ) {
isDone = true;
try {
// 在/locker下创建临时的顺序节点
ourPath = createLockNode(client, path);
System.out.println("创建临时节点======"+ourPath);
// 判断自己是否获得了锁,如果没有获得那么等待直到获得锁或者超时
hasTheLock = waitToLock(startMillis, millisToWait, ourPath);
} catch ( ZkNoNodeException e ) { // 捕获这个异常
if ( retryCount++ < MAX_RETRY_COUNT ) { // 重试指定次数
isDone = false;
} else {
throw e;
}
}
}
if ( hasTheLock ) {
return ourPath;
}
return null;
}
}
package com.dsk.modules.xcx.test;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
public class SimpleDistributedLockMutex extends BaseDistributedLock implements DistributedLock {
// 锁名称前缀,成功创建的顺序节点如lock-0000000000,lock-0000000001,...
private static final String LOCK_NAME = "lock-";
// zookeeper中locker节点的路径
private final String basePath;
// 获取锁以后自己创建的那个顺序节点的路径
private String ourLockPath;
private boolean internalLock(long time, TimeUnit unit) throws Exception {
ourLockPath = attemptLock(time, unit);
System.err.println("获取锁====="+ourLockPath);
return ourLockPath != null;
}
public SimpleDistributedLockMutex(ZkClientExt client, String basePath) {
super(client, basePath, LOCK_NAME);
this.basePath = basePath;
}
// 获取锁
public void acquire() throws Exception {
if (!internalLock(-1, null)) {
throw new IOException("连接丢失!在路径:'" + basePath + "'下不能获取锁!");
}
}
// 获取锁,可以超时
public boolean acquire(long time, TimeUnit unit) throws Exception {
return internalLock(time, unit);
}
// 释放锁
public void release() throws Exception {
releaseLock(ourLockPath);
}
}
package com.dsk.modules.xcx.test;
import java.util.Date;
import org.I0Itec.zkclient.serialize.BytesPushThroughSerializer;
import com.dsj.common.utils.DateUtils;
import com.dsk.modules.xcx.zookeeper.DistributedLock;
public class TestDistributedLock {
//模拟zk集群测试锁
public static void main(String[] args) {
Runnable runnable = new Runnable() {
public void run() {
try {
final ZkClientExt zkClientExt2 = new ZkClientExt("127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183", 50000, 50000, new BytesPushThroughSerializer());
final SimpleDistributedLockMutex mutex2 = new SimpleDistributedLockMutex(zkClientExt2, "/locks");
mutex2.acquire();
System.err.println(DateUtils.formatDateTime(new Date())+"================================="+Thread.currentThread().getName() + "正在运行");
Thread.sleep(2000);//推迟2秒关闭锁
mutex2.release();
} catch (Exception e) {
e.printStackTrace();
}
}
};
for (int i = 0; i < 10; i++) {
Thread t = new Thread(runnable);
t.start();
}
}
}