分布式锁的三种实现方式 / 分布式锁原理及实现方式 / 分布式锁1 Java常用技术方案

http://surlymo.iteye.com/blog/2082684


一、zookeeper

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. }  

 

二、memcached分布式锁

1、实现原理:

memcached带有add函数,利用add函数的特性即可实现分布式锁。add和set的区别在于:如果多线程并发set,则每个set都会成功,但最后存储的值以最后的set的线程为准。而add的话则相反,add会添加第一个到达的值,并返回true,后续的添加则都会返回false。利用该点即可很轻松地实现分布式锁。

2、优点

并发高效。

3、缺点

(1)memcached采用列入LRU置换策略,所以如果内存不够,可能导致缓存中的锁信息丢失。

(2)memcached无法持久化,一旦重启,将导致信息丢失。

 

三、redis分布式锁

redis分布式锁即可以结合zk分布式锁,锁高度安全和memcached并发场景下效率很好的优点,可以利用jedis客户端实现。参考http://blog.csdn.net/java2000_wl/article/details/8740911


发现一篇不错的文章:

利用Redisson实现分布式锁,并防止重复提交 

http://blog.csdn.net/u014042066/article/details/72830939            

关于Redisson的基础概念,参照Redisson基础

要想实现此功能需要以下几步:

1.依赖包

这里用的是jdk8+的版本

[html]
<dependency>  
  1.      <groupId>org.redisson</groupId>  
  2.      <artifactId>redisson</artifactId>  
  3.      <version>3.3.2</version>  
  4.    </dependency>  

 <dependency>
      <groupId>org.redisson</groupId>
      <artifactId>redisson</artifactId>
      <version>3.3.2</version>
    </dependency>


2.Spring整合

注意设置schema文件

[html]
  1. <redisson:client id="redissonClient2">  
  2.       
  3.        <redisson:single-server address="192.168.238.129:6379"/>  
  4.    </redisson:client>  

 <redisson:client id="redissonClient2">
     
        <redisson:single-server address="192.168.238.129:6379"/>
    </redisson:client>


3.工具类

默认锁

         


@Component  
public class DistributedRedisLock implements InitializingBean {
   
    private static final Logger _log = Logger.getLogger("DistributedRedisLock.class"); 
 
    @Autowired  
    private RedissonClient redissonClient;  
    public static Redisson redisson;  
      
    /**  
     * 获取默认锁  
     */  
    public static RLock acquireLock(String lockName) {
        RLock fairLock = redisson.getLock(lockName);  
        fairLock.lock();// 手动去解锁  
        _log.info("锁 - " + lockName + "获取成功");  
  return fairLock;
    }  
      
    /**  
     * 释放默认锁  
     */  
    public static void realeaseLock(RLock fairLock) { 
          
        fairLock.unlock();  
          
    } 
 
    @Override  
    public void afterPropertiesSet() throws Exception {  
        redisson = (Redisson) redissonClient;  
        _log.info(redisson.getConfig().toJSON());  
    }  
}  

如何防止重复呢?

  1. /** 
  2.      * 存入分布式set对象中 
  3.      * @param name 
  4.      * @param value 
  5.      * @return 
  6.      */  
  7.     public RSet<String> setSet(String name, String value){  
  8.         RSet<String> set = redisson.getSet(name);  
  9.         set.add(value);  
  10.         return  set;  
  11.     }  
  12.   
  13.     /** 
  14.      * 检查分布式对象是否存在 
  15.      * @param name 
  16.      * @return 
  17.      */  
  18.     public Boolean existSet(String name){  
  19.         RSet<String> set = redisson.getSet(name);  
  20.         if (!set.isEmpty()) {  
  21.             return  true;  
  22.         }  
  23.         return  false;  
  24.     }  

调用代码:

 String lockname="lockname";  
 RLock fairLock=DistributedRedisLock.acquireLock(lockname);
 try {
 //不存在设置到redis
 if (!DistributedRedisLock.existSet(lockname)) {
  DistributedRedisLock.setSet(lockname, "1111");
  //业务逻辑 begin
  } else {
   //redis中有则不处理业务逻辑
   }
 }finally {
    
  DistributedRedisLock.realeaseLock(fairLock);

==========================================================================

http://blog.csdn.net/zxp_cpinfo/article/details/53692922

本文转自:http://www.hollischuang.com/archives/1716

           目前几乎很多大型网站及应用都是分布式部署的,分布式场景中的数据一致性问题一直是一个比较重要的话题。分布式的CAP理论告诉我们任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多只能同时满足两项。”所以,很多系统在设计之初就要对这三者做出取舍。在互联网领域的绝大多数的场景中,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证“最终一致性”,只要这个最终时间是在用户可以接受的范围内即可。

在很多场景中,我们为了保证数据的最终一致性,需要很多的技术方案来支持,比如分布式事务、分布式锁等。有的时候,我们需要保证一个方法在同一时间内只能被同一个线程执行。在单机环境中,Java中其实提供了很多并发处理相关的API,但是这些API在分布式场景中就无能为力了。也就是说单纯的JavaApi并不能提供分布式锁的能力。所以针对分布式锁的实现目前有多种方案。

针对分布式锁的实现,目前比较常用的有以下几种方案:

基于数据库实现分布式锁 

基于缓存(Redis,memcached,tair)实现分布式锁

基于Zookeeper实现分布式锁

在分析这几种实现方案之前我们先来想一下,我们需要的分布式锁应该是怎么样的?(这里以方法锁为例,资源锁同理)

可以保证在分布式部署的应用集群中,同一个方法在同一时间只能被一台机器上的一个线程执行。

这把锁要是一把可重入锁(避免死锁)

这把锁最好是一把阻塞锁(根据业务需求考虑要不要这条)

有高可用的获取锁和释放锁功能

获取锁和释放锁的性能要好


基于数据库实现分布式锁

基于数据库表

要实现分布式锁,最简单的方式可能就是直接创建一张锁表,然后通过操作该表中的数据来实现了。

当我们要锁住某个方法或资源时,我们就在该表中增加一条记录,想要释放锁的时候就删除这条记录。

创建这样一张数据库表:

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,数据库会在查询过程中给数据库表增加排他锁(这里再多提一句,InnoDB引擎在加锁的时候,只有通过索引进行检索的时候才会使用行级锁,否则会使用表级锁。这里我们希望使用行级锁,就要给method_name添加索引,值得注意的是,这个索引一定要创建成唯一索引,否则会出现多个重载方法之间无法同时被访问的问题。重载方法的话建议把参数类型也加上。)。当某条记录被加上排他锁之后,其他线程无法再在该行记录上增加排他锁。

我们可以认为获得排它锁的线程即可获得分布式锁,当获取到锁之后,可以执行方法的业务逻辑,执行完方法之后,再通过以下方法解锁:

public void unlock(){
    connection.commit();
}

通过connection.commit()操作来释放锁。

这种方法可以有效的解决上面提到的无法释放锁和阻塞锁的问题。

  • 阻塞锁? for update语句会在执行成功后立即返回,在执行失败时一直处于阻塞状态,直到成功。
  • 锁定之后服务宕机,无法释放?使用这种方式,服务宕机之后数据库会自己把锁释放掉。

但是还是无法直接解决数据库单点和可重入问题。

这里还可能存在另外一个问题,虽然我们对method_name 使用了唯一索引,并且显示使用for update来使用行级锁。但是,mysql会对查询进行优化,即便在条件中使用了索引字段,但是否使用索引来检索数据是由 MySQL 通过判断不同执行计划的代价来决定的,如果 MySQL 认为全表扫效率更高,比如对一些很小的表,它就不会使用索引,这种情况下 InnoDB 将使用表锁,而不是行锁。如果发生这种情况就悲剧了。。。


还有一个问题,就是我们要使用排他锁来进行分布式锁的lock,那么一个排他锁长时间不提交,就会占用数据库连接。一旦类似的连接变得多了,就可能把数据库连接池撑爆

总结

总结一下使用数据库来实现分布式锁的方式,这两种方式都是依赖数据库的一张表,一种是通过表中的记录的存在情况确定当前是否有锁存在,另外一种是通过数据库的排他锁来实现分布式锁。

数据库实现分布式锁的优点

直接借助数据库,容易理解。

数据库实现分布式锁的缺点

会有各种各样的问题,在解决问题的过程中会使整个方案变得越来越复杂。

操作数据库需要一定的开销,性能问题需要考虑。

使用数据库的行级锁并不一定靠谱,尤其是当我们的锁表并不大的时候。


基于缓存实现分布式锁

相比较于基于数据库实现分布式锁的方案来说,基于缓存来实现在性能方面会表现的更好一点。而且很多缓存是可以集群部署的,可以解决单点问题。

目前有很多成熟的缓存产品,包括redis,memcached以及我们公司内部的Tair。

这里以Tair为例来分析下使用缓存实现分布式锁的方案。关于Redis和memcached在网络上有很多相关的文章,并且也有一些成熟的框架及算法可以直接使用。

基于Tair的实现分布式锁其实和Redis类似,其中主要的实现方式是使用TairManager.put方法来实现。

public boolean trylock(String key) {
    ResultCode code = ldbTairManager.put(NAMESPACE, key, "This is a Lock.", 2, 0);
    if (ResultCode.SUCCESS.equals(code))
        return true;
    else
        return false;
}
public boolean unlock(String key) {
    ldbTairManager.invalid(NAMESPACE, key);
}

以上实现方式同样存在几个问题:

1、这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在tair中,其他线程无法再获得到锁。

2、这把锁只能是非阻塞的,无论成功还是失败都直接返回。

3、这把锁是非重入的,一个线程获得锁之后,在释放锁之前,无法再次获得该锁,因为使用到的key在tair中已经存在。无法再执行put操作。

当然,同样有方式可以解决。

  • 没有失效时间?tair的put方法支持传入失效时间,到达时间之后数据会自动删除。
  • 非阻塞?while重复执行。
  • 非可重入?在一个线程获取到锁之后,把当前主机信息和线程信息保存起来,下次再获取之前先检查自己是不是当前锁的拥有者。

但是,失效时间我设置多长时间为好?如何设置的失效时间太短,方法没等执行完,锁就自动释放了,那么就会产生并发问题。如果设置的时间太长,其他获取锁的线程就可能要平白的多等一段时间。这个问题使用数据库实现分布式锁同样存在


总结

可以使用缓存来代替数据库来实现分布式锁,这个可以提供更好的性能,同时,很多缓存服务都是集群部署的,可以避免单点问题。并且很多缓存服务都提供了可以用来实现分布式锁的方法,比如Tair的put方法,redis的setnx方法等。并且,这些缓存服务也都提供了对数据的过期自动删除的支持,可以直接设置超时时间来控制锁的释放。

使用缓存实现分布式锁的优点

性能好,实现起来较为方便。

使用缓存实现分布式锁的缺点

通过超时时间来控制锁的失效时间并不是十分的靠谱。


基于Zookeeper实现分布式锁

基于zookeeper临时有序节点可以实现的分布式锁。

大致思想即为:每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。

来看下Zookeeper能不能解决前面提到的问题。

  • 锁无法释放?使用Zookeeper可以有效的解决锁无法释放的问题,因为在创建锁的时候,客户端会在ZK中创建一个临时节点,一旦客户端获取到锁之后突然挂掉(Session连接断开),那么这个临时节点就会自动删除掉。其他客户端就可以再次获得锁。

  • 非阻塞锁?使用Zookeeper可以实现阻塞的锁,客户端可以通过在ZK中创建顺序节点,并且在节点上绑定监听器,一旦节点有变化,Zookeeper会通知客户端,客户端可以检查自己创建的节点是不是当前所有节点中序号最小的,如果是,那么自己就获取到锁,便可以执行业务逻辑了。

  • 不可重入?使用Zookeeper也可以有效的解决不可重入的问题,客户端在创建节点的时候,把当前客户端的主机信息和线程信息直接写入到节点中,下次想要获取锁的时候和当前最小的节点中的数据比对一下就可以了。如果和自己的信息一样,那么自己直接获取到锁,如果不一样就再创建一个临时的顺序节点,参与排队。

  • 单点问题?使用Zookeeper可以有效的解决单点问题,ZK是集群部署的,只要集群中有半数以上的机器存活,就可以对外提供服务。

可以直接使用zookeeper第三方库Curator客户端,这个客户端中封装了一个可重入的锁服务。

public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
    try {
        return interProcessMutex.acquire(timeout, unit);
    } catch (Exception e) {
        e.printStackTrace();
    }
    return true;
}
public boolean unlock() {
    try {
        interProcessMutex.release();
    } catch (Throwable e) {
        log.error(e.getMessage(), e);
    } finally {
        executorService.schedule(new Cleaner(client, path), delayTimeForClean, TimeUnit.MILLISECONDS);
    }
    return true;
}

Curator提供的InterProcessMutex是分布式锁的实现。acquire方法用户获取锁,release方法用于释放锁。

使用ZK实现的分布式锁好像完全符合了本文开头我们对一个分布式锁的所有期望。但是,其实并不是,Zookeeper实现的分布式锁其实存在一个缺点,那就是性能上可能并没有缓存服务那么高。因为每次在创建锁和释放锁的过程中,都要动态创建、销毁瞬时节点来实现锁功能。ZK中创建和删除节点只能通过Leader服务器来执行,然后将数据同不到所有的Follower机器上。

其实,使用Zookeeper也有可能带来并发问题,只是并不常见而已。考虑这样的情况,由于网络抖动,客户端可ZK集群的session连接断了,那么zk以为客户端挂了,就会删除临时节点,这时候其他客户端就可以获取到分布式锁了。就可能产生并发问题。这个问题不常见是因为zk有重试机制,一旦zk集群检测不到客户端的心跳,就会重试,Curator客户端支持多种重试策略。多次重试之后还不行的话才会删除临时节点。(所以,选择一个合适的重试策略也比较重要,要在锁的粒度和并发之间找一个平衡。)


总结

使用Zookeeper实现分布式锁的优点

有效的解决单点问题,不可重入问题,非阻塞问题以及锁无法释放的问题。实现起来较为简单。

使用Zookeeper实现分布式锁的缺点

性能上不如使用缓存实现分布式锁。 需要对ZK的原理有所了解。


三种方案的比较

上面几种方式,哪种方式都无法做到完美。就像CAP一样,在复杂性、可靠性、性能等方面无法同时满足,所以,根据不同的应用场景选择最适合自己的才是王道。

从理解的难易程度角度(从低到高)

数据库 > 缓存 > Zookeeper

从实现的复杂性角度(从低到高)

Zookeeper >= 缓存 > 数据库

从性能角度(从高到低)

缓存 > Zookeeper >= 数据库

从可靠性角度(从高到低)

Zookeeper > 缓存 > 数据库

===============================================================================================

http://www.cnblogs.com/PurpleDream/p/5559352.html

分布式锁1 Java常用技术方案

前言:

      由于在平时的工作中,线上服务器是分布式多台部署的,经常会面临解决分布式场景下数据一致性的问题,那么就要利用分布式锁来解决这些问题。所以自己结合实际工作中的一些经验和网上看到的一些资料,做一个讲解和总结。希望这篇文章可以方便自己以后查阅,同时要是能帮助到他人那也是很好的。

 

===============================================================长长的分割线====================================================================

 

正文:

      第一步,自身的业务场景:

      在我日常做的项目中,目前涉及了以下这些业务场景:

      场景一: 比如分配任务场景。在这个场景中,由于是公司的业务后台系统,主要是用于审核人员的审核工作,并发量并不是很高,而且任务的分配规则设计成了通过审核人员每次主动的请求拉取,然后服务端从任务池中随机的选取任务进行分配。这个场景看到这里你会觉得比较单一,但是实际的分配过程中,由于涉及到了按用户聚类的问题,所以要比我描述的复杂,但是这里为了说明问题,大家可以把问题简单化理解。那么在使用过程中,主要是为了避免同一个任务同时被两个审核人员获取到的问题。我最终使用了基于数据库资源表的分布式锁来解决的问题。

      场景二: 比如支付场景。在这个场景中,我提供给用户三个用于保护用户隐私的手机号码(这些号码是从运营商处获取的,和真实手机号码看起来是一样的),让用户选择其中一个进行购买,用户购买付款后,我需要将用户选择的号码分配给用户使用,同时也要将没有选择的释放掉。在这个过程中,给用户筛选的号码要在一定时间内(用户筛选正常时间范围内)让当前用户对这个产品具有独占性,以便保证付款后是100%可以拿到;同时由于产品资源池的资源有限,还要保持资源的流动性,即不能让资源长时间被某个用户占用着。对于服务的设计目标,一期项目上线的时候至少能够支持峰值qps为300的请求,同时在设计的过程中要考虑到用户体验的问题。我最终使用了memecahed的add()方法和基于数据库资源表的分布式锁来解决的问题。

      场景三: 我有一个数据服务,每天调用量在3亿,每天按86400秒计算的qps在4000左右,由于服务的白天调用量要明显高于晚上,所以白天下午的峰值qps达到6000的,一共有4台服务器,单台qps要能达到3000以上。我最终使用了redis的setnx()和expire()的分布式锁解决的问题。

       场景四:场景一和场景二的升级版。在这个场景中,不涉及支付。但是由于资源分配一次过程中,需要保持涉及一致性的地方增加,而且一期的设计目标要达到峰值qps500,所以需要我们对场景进一步的优化。我最终使用了redis的setnx()、expire()和基于数据库表的分布式锁来解决的问题。

 

      看到这里,不管你觉得我提出的业务场景qps是否足够大,都希望你能继续看下去,因为无论你身处一个什么样的公司,最开始的工作可能都需要从最简单的做起。不要提阿里和腾讯的业务场景qps如何大,因为在这样的大场景中你未必能亲自参与项目,亲自参与项目未必能是核心的设计者,是核心的设计者未必能独自设计。如果能真能满足以上三条,关闭页面可以不看啦,如果不是的话,建议还是看完,我有说的不足的地方欢迎提出建议,我说的好的地方,也希望给我点个赞或者评论一下,算是对我最大的鼓励哈。

 

  第二步,分布式锁的解决方式:

      1. 首先明确一点,有人可能会问是否可以考虑采用ReentrantLock来实现,但是实际上去实现的时候是有问题的,ReentrantLock的lock和unlock要求必须是在同一线程进行,而分布式应用中,lock和unlock是两次不相关的请求,因此肯定不是同一线程,因此导致无法使用ReentrantLock。

      2. 基于数据库表做乐观锁,用于分布式锁。

      3. 使用memcached的add()方法,用于分布式锁。

      4. 使用memcached的cas()方法,用于分布式锁。(不常用) 

      5. 使用redis的setnx()、expire()方法,用于分布式锁。

      6. 使用redis的setnx()、get()、getset()方法,用于分布式锁。

      7. 使用redis的watch、multi、exec命令,用于分布式锁。(不常用) 

      8. 使用zookeeper,用于分布式锁。(不常用) 

     

      第三步,基于数据库资源表做乐观锁,用于分布式锁:

      1. 首先说明乐观锁的含义:

          大多数是基于数据版本(version)的记录机制实现的。何谓数据版本号?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表添加一个 “version”字段来实现读取出数据时,将此版本号一同读出,之后更新时,对此版本号加1。

          在更新过程中,会对版本号进行比较,如果是一致的,没有发生改变,则会成功执行本次操作;如果版本号不一致,则会更新失败。

      2. 对乐观锁的含义有了一定的了解后,结合具体的例子,我们来推演下我们应该怎么处理:

          (1). 假设我们有一张资源表,如下图所示: t_resource , 其中有6个字段id, resoource,  state, add_time, update_time, version,分别表示表主键、资源、分配状态(1未分配  2已分配)、资源创建时间、资源更新时间、资源数据版本号。

          

         (4). 假设我们现在我们对id=5780这条数据进行分配,那么非分布式场景的情况下,我们一般先查询出来state=1(未分配)的数据,然后从其中选取一条数据可以通过以下语句进行,如果可以更新成功,那么就说明已经占用了这个资源

               update t_resource set state=2 where state=1 and id=5780。

         (5). 如果在分布式场景中,由于数据库的update操作是原子是原子的,其实上边这条语句理论上也没有问题,但是这条语句如果在典型的“ABA”情况下,我们是无法感知的。有人可能会问什么是“ABA”问题呢?大家可以网上搜索一下,这里我说简单一点就是,如果在你第一次select和第二次update过程中,由于两次操作是非原子的,所以这过程中,如果有一个线程,先是占用了资源(state=2),然后又释放了资源(state=1),实际上最后你执行update操作的时候,是无法知道这个资源发生过变化的。也许你会说这个在你说的场景中应该也还好吧,但是在实际的使用过程中,比如银行账户存款或者扣款的过程中,这种情况是比较恐怖的。

         (6). 那么如果使用乐观锁我们如何解决上边的问题呢?

               a. 先执行select操作查询当前数据的数据版本号,比如当前数据版本号是26:

                   select id, resource, state,version from t_resource  where state=1 and id=5780;

               b. 执行更新操作:

                   update t_resoure set state=2, version=27, update_time=now() where resource=xxxxxx and state=1 and version=26

               c. 如果上述update语句真正更新影响到了一行数据,那就说明占位成功。如果没有更新影响到一行数据,则说明这个资源已经被别人占位了。

      3. 通过2中的讲解,相信大家已经对如何基于数据库表做乐观锁有有了一定的了解了,但是这里还是需要说明一下基于数据库表做乐观锁的一些缺点:

          (1). 这种操作方式,使原本一次的update操作,必须变为2次操作: select版本号一次;update一次。增加了数据库操作的次数。

          (2). 如果业务场景中的一次业务流程中,多个资源都需要用保证数据一致性,那么如果全部使用基于数据库资源表的乐观锁,就要让每个资源都有一张资源表,这个在实际使用场景中肯定是无法满足的。而且这些都基于数据库操作,在高并发的要求下,对数据库连接的开销一定是无法忍受的。

          (3). 乐观锁机制往往基于系统中的数据存储逻辑,因此可能会造成脏数据被更新到数据库中。在系统设计阶段,我们应该充分考虑到这些情况出现的可能性,并进行相应调整,如将乐观锁策略在数据库存储过程中实现,对外只开放基于此存储过程的数据更新途径,而不是将数据库表直接对外公开。     

      4. 讲了乐观锁的实现方式和缺点,是不是会觉得不敢使用乐观锁了呢???当然不是,在文章开头我自己的业务场景中,场景1和场景2的一部分都使用了基于数据库资源表的乐观锁,已经很好的解决了线上问题。所以大家要根据的具体业务场景选择技术方案,并不是随便找一个足够复杂、足够新潮的技术方案来解决业务问题就是好方案?!比如,如果在我的场景一中,我使用zookeeper做锁,可以这么做,但是真的有必要吗???答案觉得是没有必要的!!!

 

      第四步,使用memcached的add()方法,用于分布式锁:

      对于使用memcached的add()方法做分布式锁,这个在互联网公司是一种比较常见的方式,而且基本上可以解决自己手头上的大部分应用场景。在使用这个方法之前,只要能搞明白memcached的add()和set()的区别,并且知道为什么能用add()方法做分布式锁就好。如果还不知道add()和set()方法,请直接百度吧,这个需要自己了解一下。

      我在这里想说明的是另外一个问题,人们在关注分布式锁设计的好坏时,还会重点关注这样一个问题,那就是是否可以避免死锁问题???!!!

      如果使用memcached的add()命令对资源占位成功了,那么是不是就完事儿了呢?当然不是!我们需要在add()的使用指定当前添加的这个key的有效时间,如果不指定有效时间,正常情况下,你可以在执行完自己的业务后,使用delete方法将这个key删除掉,也就是释放了占用的资源。但是,如果在占位成功后,memecached或者自己的业务服务器发生宕机了,那么这个资源将无法得到释放。所以通过对key设置超时时间,即便发生了宕机的情况,也不会将资源一直占用,可以避免死锁的问题。

     

      第五步,使用memcached的cas()方法,用于分布式锁:     

      下篇文章我们再细说!

 

      第六步,使用redis的setnx()、expire()方法,用于分布式锁:

      对于使用redis的setnx()、expire()来实现分布式锁,这个方案相对于memcached()的add()方案,redis占优势的是,其支持的数据类型更多,而memcached只支持String一种数据类型。除此之外,无论是从性能上来说,还是操作方便性来说,其实都没有太多的差异,完全看你的选择,比如公司中用哪个比较多,你就可以用哪个。

      首先说明一下setnx()命令,setnx的含义就是SET if Not Exists,其主要有两个参数 setnx(key, value)。该方法是原子的,如果key不存在,则设置当前key成功,返回1;如果当前key已经存在,则设置当前key失败,返回0。但是要注意的是setnx命令不能设置key的超时时间,只能通过expire()来对key设置。

      具体的使用步骤如下:

      1. setnx(lockkey, 1)  如果返回0,则说明占位失败;如果返回1,则说明占位成功

      2. expire()命令对lockkey设置超时时间,为的是避免死锁问题。

      3. 执行完业务代码后,可以通过delete命令删除key。

      这个方案其实是可以解决日常工作中的需求的,但从技术方案的探讨上来说,可能还有一些可以完善的地方。比如,如果在第一步setnx执行成功后,在expire()命令执行成功前,发生了宕机的现象,那么就依然会出现死锁的问题,所以如果要对其进行完善的话,可以使用redis的setnx()、get()和getset()方法来实现分布式锁。   

 

      第七步,使用redis的setnx()、get()、getset()方法,用于分布式锁:

      这个方案的背景主要是在setnx()和expire()的方案上针对可能存在的死锁问题,做了一版优化。

      那么先说明一下这三个命令,对于setnx()和get()这两个命令,相信不用再多说什么。那么getset()命令?这个命令主要有两个参数 getset(key,newValue)。该方法是原子的,对key设置newValue这个值,并且返回key原来的旧值。假设key原来是不存在的,那么多次执行这个命令,会出现下边的效果:

      1. getset(key, "value1")  返回nil   此时key的值会被设置为value1

      2. getset(key, "value2")  返回value1   此时key的值会被设置为value2

      3. 依次类推!

      介绍完要使用的命令后,具体的使用步骤如下:

      1. setnx(lockkey, 当前时间+过期超时时间) ,如果返回1,则获取锁成功;如果返回0则没有获取到锁,转向2。

      2. get(lockkey)获取值oldExpireTime ,并将这个value值与当前的系统时间进行比较,如果小于当前系统时间,则认为这个锁已经超时,可以允许别的请求重新获取,转向3。

      3. 计算newExpireTime=当前时间+过期超时时间,然后getset(lockkey, newExpireTime) 会返回当前lockkey的值currentExpireTime。

      4. 判断currentExpireTime与oldExpireTime 是否相等,如果相等,说明当前getset设置成功,获取到了锁。如果不相等,说明这个锁又被别的请求获取走了,那么当前请求可以直接返回失败,或者继续重试。

      5. 在获取到锁之后,当前线程可以开始自己的业务处理,当处理完毕后,比较自己的处理时间和对于锁设置的超时时间,如果小于锁设置的超时时间,则直接执行delete释放锁;如果大于锁设置的超时时间,则不需要再锁进行处理。

      注意: 这个方案我当初在线上使用的时候是没有问题的,所以当初写这篇文章时也认为是没有问题的。但是截止到2017.05.13(周六),自己在重新回顾这篇文章时,看了文章下网友的很多评论,我发现有两个问题比较集中:

      问题1:  在“get(lockkey)获取值oldExpireTime ”这个操作与“getset(lockkey, newExpireTime) ”这个操作之间,如果有N个线程在get操作获取到相同的oldExpireTime后,然后都去getset,会不会返回的newExpireTime都是一样的,都会是成功,进而都获取到锁???

      我认为这套方案是不存在这个问题的。依据有两条: 第一,redis是单进程单线程模式,串行执行命令。 第二,在串行执行的前提条件下,getset之后会比较返回的currentExpireTime与oldExpireTime 是否相等。

      问题2: 在“get(lockkey)获取值oldExpireTime ”这个操作与“getset(lockkey, newExpireTime) ”这个操作之间,如果有N个线程在get操作获取到相同的oldExpireTime后,然后都去getset,假设第1个线程获取锁成功,其他锁获取失败,但是获取锁失败的线程它发起的getset命令确实执行了,这样会不会造成第一个获取锁的线程设置的锁超时时间一直在延长???

      我认为这套方案确实存在这个问题的可能。但我个人认为这个微笑的误差是可以忽略的,不过技术方案上存在缺陷,大家可以自行抉择哈。

 

      第八步,使用redis的watch、multi、exec命令,用于分布式锁:

      下篇文章我们再细说!

 

      第九步,使用zookeeper,用于分布式锁:

      下篇文章我们再细说!

 

      第十步,总结:

      综上,关于分布式锁的第一篇文章我就写到这儿了,在文章中主要说明了日常项目中会比较常用到四种方案,大家掌握了这四种方案,其实在日常的工作中就可以解决很多业务场景下的分布式锁的问题。从文章开头我自己的实际使用中,也可以看到,这么说完全是有一定的依据。对于另外那三种方案,我会在下一篇关于分布式锁的文章中,和大家再探讨一下。

      常用的四种方案:

      1. 基于数据库表做乐观锁,用于分布式锁。

      2. 使用memcached的add()方法,用于分布式锁。

      3. 使用redis的setnx()、expire()方法,用于分布式锁。

      4. 使用redis的setnx()、get()、getset()方法,用于分布式锁。

      不常用但是可以用于技术方案探讨的:

      1. 使用memcached的cas()方法,用于分布式锁。 

      2. 使用redis的watch、multi、exec命令,用于分布式锁。

      3. 使用zookeeper,用于分布式锁。

 

      转载请注明来自博客园http://www.cnblogs.com/PurpleDream/p/5559352.html ,版权归本人和博客园所有,谢谢!



  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值