分布式锁的三种实现方法

来源:互联网 发布:ubuntu镜像文件iso下载 编辑:程序博客网 时间:2024/06/06 02:30

目前几乎所有的大型web应用全都是分布式部署的,分布式场景中的数据一致性问题一直是一个比较重要的话题。分布式应用中的CAP理论告诉我们:

任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(Availability)、分区容错性(Partation tolerance)。最多只能同时满足其中两项。

所以在设计之初,就需要对三者做出取舍。一般在互联网场景中,都会选择牺牲强一致性,来换取高可用性,系统只需要保证最终一致性

那么问题就是,如何保证最终一致性?通常的技术方案有分布式事务,分布式锁。我们需要保证,在同一时刻,只有一个线程执行某一个特定方法。我们知道,在单机环境中,或者是单JVM中,java的并发api可以保证并发安全。但是在集群情况下,java的并发api就没办法了。我们需要分布式锁。

通常分布式锁的实现方案有三种:

1.基于数据库实现 
2.基于缓存(redis、memcached、tair) 
3.基于zookeeper

在思考方案前,首先要考虑目标,我们需要什么样的分布式锁?

1、安全,要保证加锁的方案同一时刻只能被一台机器上的一个线程访问。 
2、可重入锁,避免死锁 
3、最好是阻塞锁 
4、高可用的获取锁和释放锁功能,尤其要关注释放锁的可靠性。

1.基于数据库的实现

基于数据库表

这应该是最直接能想到的方法。 
创建一张表,基于表的数据操作锁。 
当我们要加锁是,创建一条记录。释放锁时,删除这条记录。

  1. 创建表
CREATE TABLE `myLock`(`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='锁定中的方法'
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

当我们需要锁住某个方法时,执行插入。

insert into myLock(method_name,desc) values ('theMethod','the desc');
  • 1

当需要释放锁时,执行delete。

delete from myLock where method_name =  theMethod
  • 1

这种实现有什么问题?

  1. 该方案强依赖于数据库的可用性。数据库是单点,一旦挂掉,将导致整个业务不可用。
  2. 该锁不是阻塞的。依赖insert操作,一旦失败直接返回。没有获得锁的线程不会进入队列,要想获取锁,只能再次发起insert。
  3. 该锁没有失效时间。一旦delete失败,锁就永远存在了。
  4. 该锁不是可重入锁。

有没有解决方法?

  1. 数据库主备,自动切换。增加了方案复杂性,增加了money投入。
  2. 搞个while循环
  3. 定时任务
  4. 加字段。记录主机信息和线程现象,下一次获取的时候比对一下。

基于数据库排它锁

原理:利用数据库自带的排他锁机制。select for update。 
还是先建了上面那张表。 
在mysql的InnoDB引擎下,可以用下面的代码:

public boolean lock(){    connection.setAutoCommit(false)    while(true){        try{            result = select * from myLock where method_name=xxx for update;            if(result==null){                return true;            }        }catch(Exception e){        }        sleep(1000);    }    return false;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

这样可以给某条记录加上排他锁,之后其他的线程就不可以再加排它锁了。要注意的是,InnoDB在加锁的时候只有通过索引检索的时候才会使用行级锁,否则表级锁。这个索引一定要建成唯一索引,否则会出现多个重载方法无法访问的情况,重载方法就再加个参数类型字段。

释放锁的代码:

public void unlock(){    connection.commit();}
  • 1
  • 2
  • 3
  • 4

有没有什么问题?

  1. 单点问题依然存在
  2. 天生是阻塞的。for update失败会一直处于阻塞状态。
  3. 服务宕机之后,数据库自己会把锁释放掉。
  4. 依然是不可重入锁。
  5. 另外要注意数据库执行时的优化。你建了索引,数据库执行时不一定用的。

总结:

数据库这种方式看起来简单,实际不会采用的。因为要埋这些坑,要加入更多的代码。使方案变得越来越复杂。

2.基于缓存实现

基于缓存实现,在性能上回比较好,而且缓存一般都是集群部署的,解决了单点问题。 
例如,redis使用setnx,expire。 
例子很多,但是在生产环境中,没有用过。setnx不是原子的。exire超时也不是很靠谱。

可以参考本博的另外一篇博文关于基于Redis的分布式锁的实现:

3.基于Zookeeper实现

当某个客户端获取锁后,突然挂掉。没关系,对应的znode会被自动删除,不会出现锁释放不了的问题。 
是阻塞锁 
可重入 
集群部署

1、实现原理:

基于zookeeper瞬时有序节点实现的分布式锁,其主要逻辑如下(该图来自于IBM网站)。大致思想即为:每个客户端对某个功能加锁时,

在zookeeper上的与该功能对应的指定节点的目录下,生成一个唯一的瞬时有序节点。判断是否获取锁的方式很简单,

只需要判断有序节点中序号最小的一个。当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,

而产生的死锁问题。

2、优点

锁安全性高,zk可持久化

3、缺点

性能开销比较高。因为其需要动态产生、销毁瞬时节点来实现锁功能。

4、实现

可以直接采用zookeeper第三方库curator即可方便地实现分布式锁。以下为基于curator实现的zk分布式锁核心代码:

 

Java代码  
  1. @Override  
  2. public boolean tryLock(LockInfo info) {  
  3.     InterProcessMutex mutex = getMutex(info);  
  4.     int tryTimes = info.getTryTimes();  
  5.     long tryInterval = info.getTryInterval();  
  6.     boolean flag = true;// 代表是否需要重试  
  7.     while (flag && --tryTimes >= 0) {  
  8.         try {  
  9.             if (mutex.acquire(info.getWaitLockTime(), TimeUnit.MILLISECONDS)) {  
  10.                 LOGGER.info(LogConstant.DST_LOCK + "acquire lock successfully!");  
  11.                 flag = false;  
  12.                 break;  
  13.             }  
  14.         } catch (Exception e) {  
  15.             LOGGER.error(LogConstant.DST_LOCK + "acquire lock error!", e);  
  16.         } finally {  
  17.             checkAndRetry(flag, tryInterval, tryTimes);  
  18.         }  
  19.     }  
  20.     return !flag;// 最后还需要重试,说明没拿到锁  
  21. }  

 

Java代码  
  1. @Override  
  2. public boolean releaseLock(LockInfo info) {  
  3.     InterProcessMutex mutex = getMutex(info);  
  4.     int tryTimes = info.getTryTimes();  
  5.     long tryInterval = info.getTryInterval();  
  6.     boolean flag = true;// 代表是否需要重试  
  7.     while (flag && --tryTimes >= 0) {  
  8.         try {  
  9.             mutex.release();  
  10.             LOGGER.info(LogConstant.DST_LOCK + "release lock successfully!");  
  11.             flag = false;  
  12.             break;  
  13.         } catch (Exception e) {  
  14.             LOGGER.error(LogConstant.DST_LOCK + "release lock error!", e);  
  15.         } finally {  
  16.             checkAndRetry(flag, tryInterval, tryTimes);  
  17.         }  
  18.     }  
  19.     return !flag;// 最后还需要重试,说明没拿到锁  
  20. }  

 

Java代码  
  1. /** 
  2.      * 获取锁。此处需要加同步,concurrentHashmap无法避免此处的同步问题 
  3.      * @param info 锁信息 
  4.      * @return 锁实例 
  5.      */  
  6.     private synchronized InterProcessMutex getMutex(LockInfo info) {  
  7.         InterProcessReadWriteLock lock = null;  
  8.         if (locksCache.get(info.getLock()) != null) {  
  9.             lock = locksCache.get(info.getLock());  
  10.         } else {  
  11.             lock = new InterProcessReadWriteLock(client, BASE_DIR + info.getLock());  
  12.             locksCache.put(info.getLock(), lock);  
  13.         }  
  14.         InterProcessMutex mutex = null;  
  15.         switch (info.getIsolate()) {  
  16.         case READ:  
  17.             mutex = lock.readLock();  
  18.             break;  
  19.         case WRITE:  
  20.             mutex = lock.writeLock();  
  21.             break;  
  22.         default:  
  23.             throw new IllegalArgumentException();  
  24.         }  
  25.         return mutex;  
  26.     }  

 

Java代码  
  1. /** 
  2.  * 判断是否需要重试 
  3.  * @param flag 是否需要重试标志 
  4.  * @param tryInterval 重试间隔 
  5.  * @param tryTimes 重试次数 
  6.  */  
  7. private void checkAndRetry(boolean flag, long tryInterval, int tryTimes) {  
  8.     try {  
  9.         if (flag) {  
  10.             Thread.sleep(tryInterval);  
  11.             LOGGER.info(LogConstant.DST_LOCK + "retry getting lock! now retry time left: " + tryTimes);  
  12.         }  
  13.     } catch (InterruptedException e) {  
  14.         LOGGER.error(LogConstant.DST_LOCK + "retry interval thread interruptted!", e);  
  15.     }  
  16. }  

但是生产环境上还是推荐zookeeper。

关于ZK实现的分布式锁的性能,可以参考这方篇文章:

一个基于zookeeper实现的分布式锁的性能测试


原创粉丝点击