分布式锁

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缓存实现分布式锁

请参考:http://www.cnblogs.com/0201zcr/p/5942748.html

基于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();
        }
    }

}
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值