精华内容
下载资源
问答
  • 三种实现分布式锁的方式

    万次阅读 多人点赞 2018-06-14 15:01:57
    一、为什么要使用分布式锁我们在开发应用的时候,如果需要对某一个共享变量进行多线程同步访问的时候,可以使用我们学到的Java多线程的18般武艺进行处理,并且可以完美的运行,毫无Bug!注意这是单机应用,也就是...

    一、为什么要使用分布式锁

    我们在开发应用的时候,如果需要对某一个共享变量进行多线程同步访问的时候,可以使用我们学到的Java多线程的18般武艺进行处理,并且可以完美的运行,毫无Bug!

    注意这是单机应用,也就是所有的请求都会分配到当前服务器的JVM内部,然后映射为操作系统的线程进行处理!而这个共享变量只是在这个JVM内部的一块内存空间!

    后来业务发展,需要做集群,一个应用需要部署到几台机器上然后做负载均衡,大致如下图:

    这里写图片描述

    上图可以看到,变量A存在JVM1、JVM2、JVM3三个JVM内存中(这个变量A主要体现是在一个类中的一个成员变量,是一个有状态的对象,例如:UserController控制器中的一个整形类型的成员变量),如果不加任何控制的话,变量A同时都会在JVM分配一块内存,三个请求发过来同时对这个变量操作,显然结果是不对的!即使不是同时发过来,三个请求分别操作三个不同JVM内存区域的数据,变量A之间不存在共享,也不具有可见性,处理的结果也是不对的!

    如果我们业务中确实存在这个场景的话,我们就需要一种方法解决这个问题!

    为了保证一个方法或属性在高并发情况下的同一时间只能被同一个线程执行,在传统单体应用单机部署的情况下,可以使用Java并发处理相关的API(如ReentrantLock或Synchronized)进行互斥控制。在单机环境中,Java中提供了很多并发处理相关的API。但是,随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的Java API并不能提供分布式锁的能力。为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!

    二、分布式锁应该具备哪些条件

    在分析分布式锁的三种实现方式之前,先了解一下分布式锁应该具备哪些条件:

    1、在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行; 
    2、高可用的获取锁与释放锁; 
    3、高性能的获取锁与释放锁; 
    4、具备可重入特性; 
    5、具备锁失效机制,防止死锁; 
    6、具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。

    三、分布式锁的三种实现方式
     

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

     

    在很多场景中,我们为了保证数据的最终一致性,需要很多的技术方案来支持,比如分布式事务、分布式锁等。有的时候,我们需要保证一个方法在同一时间内只能被同一个线程执行。

    基于数据库实现分布式锁; 
    基于缓存(Redis等)实现分布式锁; 
    基于Zookeeper实现分布式锁;

     

    1.基于数据库实现排他锁

    方案1

     表结构


    获取锁

     

    INSERT INTO method_lock (method_name, desc) VALUES ('methodName', 'methodName');

    对method_name做了唯一性约束,这里如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功。

    方案2
    表结构

    DROP TABLE IF EXISTS `method_lock`;
    CREATE TABLE `method_lock` (
      `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
      `method_name` varchar(64) NOT NULL COMMENT '锁定的方法名',
      `state` tinyint NOT NULL COMMENT '1:未分配;2:已分配',
      `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
      `version` int NOT NULL COMMENT '版本号',
      `PRIMARY KEY (`id`),
      UNIQUE KEY `uidx_method_name` (`method_name`) USING BTREE
    ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='锁定中的方法';

    先获取锁的信息
         select id, method_name, state,version from method_lock where state=1 and method_name='methodName';

    占有锁
           update t_resoure set state=2, version=2, update_time=now() where method_name='methodName' and state=1 and version=2;

    如果没有更新影响到一行数据,则说明这个资源已经被别人占位了。

    缺点

        1、这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。
        2、这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。
        3、这把锁只能是非阻塞的,因为数据的insert操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作。
        4、这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。

    解决方案
         1、数据库是单点?搞两个数据库,数据之前双向同步。一旦挂掉快速切换到备库上。
         2、没有失效时间?只要做一个定时任务,每隔一定时间把数据库中的超时数据清理一遍。
         3、非阻塞的?搞一个while循环,直到insert成功再返回成功。
         4、非重入的?在数据库表中加个字段,记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了。

     

     

    2.基于redis实现


    获取锁使用命令:

     

    SET resource_name my_random_value NX PX 30000

    方案

    try{
    	lock = redisTemplate.opsForValue().setIfAbsent(lockKey, LOCK);
    	logger.info("cancelCouponCode是否获取到锁:"+lock);
    	if (lock) {
    		// TODO
    		redisTemplate.expire(lockKey,1, TimeUnit.MINUTES); //成功设置过期时间
    		return res;
    	}else {
    		logger.info("cancelCouponCode没有获取到锁,不执行任务!");
    	}
    }finally{
    	if(lock){	
    		redisTemplate.delete(lockKey);
    		logger.info("cancelCouponCode任务结束,释放锁!");		
    	}else{
    		logger.info("cancelCouponCode没有获取到锁,无需释放锁!");
    	}
    }

     

    缺点:

    在这种场景(主从结构)中存在明显的竞态:
        客户端A从master获取到锁,
        在master将锁同步到slave之前,master宕掉了。
        slave节点被晋级为master节点,
        客户端B取得了同一个资源被客户端A已经获取到的另外一个锁。安全失效

     


    3.基于zookeeper实现
     

    让我们来回顾一下Zookeeper节点的概念:

     

     

    「每日分享」如何用Zookeeper实现分布式锁

     

     

    Zookeeper的数据存储结构就像一棵树,这棵树由节点组成,这种节点叫做Znode。

    Znode分为四种类型:

    1.持久节点 (PERSISTENT)

    默认的节点类型。创建节点的客户端与zookeeper断开连接后,该节点依旧存在 。

    2.持久节点顺序节点(PERSISTENT_SEQUENTIAL)

    所谓顺序节点,就是在创建节点时,Zookeeper根据创建的时间顺序给该节点名称进行编号:

    「每日分享」如何用Zookeeper实现分布式锁

     

    3.临时节点(EPHEMERAL)

    和持久节点相反,当创建节点的客户端与zookeeper断开连接后,临时节点会被删除:

    「每日分享」如何用Zookeeper实现分布式锁

     

    「每日分享」如何用Zookeeper实现分布式锁

     

    「每日分享」如何用Zookeeper实现分布式锁

     

    4.临时顺序节点(EPHEMERAL_SEQUENTIAL)

    顾名思义,临时顺序节点结合和临时节点和顺序节点的特点:在创建节点时,Zookeeper根据创建的时间顺序给该节点名称进行编号;当创建节点的客户端与zookeeper断开连接后,临时节点会被删除。

    Zookeeper分布式锁的原理

    Zookeeper分布式锁恰恰应用了临时顺序节点。具体如何实现呢?让我们来看一看详细步骤:

    获取锁

    首先,在Zookeeper当中创建一个持久节点ParentLock。当第一个客户端想要获得锁时,需要在ParentLock这个节点下面创建一个临时顺序节点 Lock1。

    「每日分享」如何用Zookeeper实现分布式锁

     

    之后,Client1查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock1是不是顺序最靠前的一个。如果是第一个节点,则成功获得锁。

    「每日分享」如何用Zookeeper实现分布式锁

     

    这时候,如果再有一个客户端 Client2 前来获取锁,则在ParentLock下载再创建一个临时顺序节点Lock2。

    「每日分享」如何用Zookeeper实现分布式锁

     

    Client2查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock2是不是顺序最靠前的一个,结果发现节点Lock2并不是最小的。

    于是,Client2向排序仅比它靠前的节点Lock1注册Watcher,用于监听Lock1节点是否存在。这意味着Client2抢锁失败,进入了等待状态。

    「每日分享」如何用Zookeeper实现分布式锁

     

    这时候,如果又有一个客户端Client3前来获取锁,则在ParentLock下载再创建一个临时顺序节点Lock3。

    「每日分享」如何用Zookeeper实现分布式锁

     

    Client3查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock3是不是顺序最靠前的一个,结果同样发现节点Lock3并不是最小的。

    于是,Client3向排序仅比它靠前的节点Lock2注册Watcher,用于监听Lock2节点是否存在。这意味着Client3同样抢锁失败,进入了等待状态。

    「每日分享」如何用Zookeeper实现分布式锁

     

    这样一来,Client1得到了锁,Client2监听了Lock1,Client3监听了Lock2。这恰恰形成了一个等待队列,很像是Java当中ReentrantLock所依赖的

    释放锁

    释放锁分为两种情况:

    1.任务完成,客户端显示释放

    当任务完成时,Client1会显示调用删除节点Lock1的指令。

    「每日分享」如何用Zookeeper实现分布式锁

     

    2.任务执行过程中,客户端崩溃

    获得锁的Client1在任务执行过程中,如果Duang的一声崩溃,则会断开与Zookeeper服务端的链接。根据临时节点的特性,相关联的节点Lock1会随之自动删除。

    「每日分享」如何用Zookeeper实现分布式锁

     

    由于Client2一直监听着Lock1的存在状态,当Lock1节点被删除,Client2会立刻收到通知。这时候Client2会再次查询ParentLock下面的所有节点,确认自己创建的节点Lock2是不是目前最小的节点。如果是最小,则Client2顺理成章获得了锁。

    「每日分享」如何用Zookeeper实现分布式锁

     

    同理,如果Client2也因为任务完成或者节点崩溃而删除了节点Lock2,那么Client3就会接到通知。

    「每日分享」如何用Zookeeper实现分布式锁

     

    最终,Client3成功得到了锁。

    方案:

     

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

     

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

    https://github.com/apache/curator/

     

    缺点:

        性能上可能并没有缓存服务那么高。因为每次在创建锁和释放锁的过程中,都要动态创建、销毁瞬时节点来实现锁功能。ZK中创建和删除节点只能通过Leader服务器来执行,然后将数据同不到所有的Follower机器上。

     

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

     

    4.总结

    下面的表格总结了Zookeeper和Redis分布式锁的优缺点:

     

     

    三种方案的比较

     

     

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

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

    数据库 > 缓存 > Zookeeper

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

    Zookeeper >= 缓存 > 数据库

    从性能角度(从高到低)

    缓存 > Zookeeper >= 数据库

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

    Zookeeper > 缓存 > 数据库

     

     

     


    Refrence:
    https://www.cnblogs.com/austinspark-jessylu/p/8043726.html
    https://www.toutiao.com/a6558681932786303501/?tt_from=weixin&utm_campaign=client_share×tamp=1528800534&app=news_article&utm_source=weixin&iid=34667892860&utm_medium=toutiao_ios&wxshare_count=1

     

     

     

     

     

     

     

     

     

     

     

     

    展开全文
  • 分布式锁

    万次阅读 2020-05-09 15:44:37
    分布式锁的介绍和实现的两种方式:基于RestTemplate和Reddsion

    一、分布式锁的简介

    在分布式系统中数据一致性显得格外重要。分布式的CAP理论告诉我们“任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多只能同时满足两项”。而P是一定需要满足的,在CA,大多数公司会选择A,牺牲强一致性来换取系统的高可用性,系统往往只需要保证"最终一致性"。

    为了保证数据的最终一致性,需要很多的技术方案来支持,比如分布式事务、分布式锁等。分布式事务现在有很多解决方案:SeataGTSSaga…分布式锁的实现不能再依靠SycrhonizedJUC去实现,因为分布式环境是多个JVM,单单是用这些已经无法满足,本文介绍基于RedisZookeeper实现的几种分布式锁方案。

    二、分布式锁实现方案

    在这里插入图片描述

    2.1 基于 springboot 整合的 RedisTemplate

    这里我们举了一个分布式系统下删减商品库存的案例。大致实现的原理就是使用reidssetnx功能去上锁。这里给这个锁加上超时时间是防止单体故障后锁无法被清除。当没有没有获取到分布式锁的线程让我做自旋操作或者返回错误提示页面。最后finally代码块中删除自己存入的分布式锁。

    在分布式环境下我们需要考虑:

    • 获取锁和加锁需保证原子性setex
    • 使用Lua脚本可以保证加锁(过期时间)和删锁的原子性
    • 如何保证锁的续期(使用RedisTemplate无法实现)
    	@Autowired
        private StringRedisTemplate redisTemplate;
    
        @GetMapping("reduce_stock")
        public void reduceStock() {
            String token = "redis_lock";
            String clientID = null;
            try {
                clientID = UUID.randomUUID().toString();
                Boolean setIfAbsent = redisTemplate.opsForValue().setIfAbsent(token, clientID, 30, TimeUnit.SECONDS);
                do {
                    //自旋(这里可以休眠或者异常)
                    try {
                        TimeUnit.MILLISECONDS.sleep(300);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } while (!setIfAbsent);
                //业务操作:模拟减库存
                int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
                if (stock > 0) {
                    redisTemplate.opsForValue().set("stock", String.valueOf(stock - 1));
                } else {
                    //给出提示库存不足,return "error";
                }
            } finally {
                //删锁
                String lua = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
                        "then\n" +
                        "    return redis.call(\"del\",KEYS[1])\n" +
                        "else\n" +
                        "    return 0\n" +
                        "end";
                RedisScript<Long> luaScript = RedisScript.of(lua, Long.class);
                redisTemplate.execute(luaScript, Arrays.asList(token), clientID);
            }
        }
    

    2.2 基于 Redisson

    Redisson是一个基于java编程框架netty进行扩展了的Redis,适用于分布式环境下对Redis的操作。

    基于RedisTemplate的分布式锁方式有一种缺点,就是锁的过期时间的设置不好确定,适用Redisson可以很好的解决这个缺点。当一个线程获取到分布式锁后,其他线程会一直自旋。获取到分布式的锁的线程,后台每隔10秒查看是否失效,若没失效则延长时间,默认失效时间为30秒。

    引入依赖:

    <dependency>
        <groupId>org.redisson</groupId>
        <artifactId>redisson</artifactId>
        <version>3.13.6</version>
    </dependency>
    
    @Controller
    public class RedissonLockTest {
    
        private static final String lockKey = "redis_lock";
    
        @Autowired
        private Redisson redisson;
    
        @GetMapping("reduce_stock")
        public void reduceStock() {
            RLock lock = null;
            try {
                lock = redisson.getLock(lockKey);
                lock.lock();
                int stock = Integer.parseInt((redisson.getBucket("stock").get().toString()));
                if (stock > 0) {
                    redisson.getBucket("stock").set(stock - 1);
                } else {
                    //给出提示库存不足,return "error";
                }
            } finally {
                lock.unlock();
            }
        }
    }
    

    使用Redisson其他分布式锁参考文档:https://github.com/redisson/redisson/wiki/8.-%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E5%92%8C%E5%90%8C%E6%AD%A5%E5%99%A8

    展开全文
  • Java基于Redis实现分布式锁

    万次阅读 多人点赞 2019-07-31 19:12:35
    分布式锁可以基于很多种方式实现,比如zookeeper、redis...。不管哪种方式,他的基本原理是不变的:用一个状态值表示锁,对锁的占用和释放通过状态值来标识。 一、为什么Redis可以方便地实现分布式锁 1、Redis为...

    分布式锁可以基于很多种方式实现,比如zookeeper、redis...。不管哪种方式,他的基本原理是不变的:用一个状态值表示锁,对锁的占用和释放通过状态值来标识。

    一、为什么Redis可以方便地实现分布式锁

    1、Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系。

    2、Redis的SETNX命令可以方便的实现分布式锁。

    setNX(SET if Not eXists)

    语法:SETNX key value

    返回值:设置成功,返回 1 ;设置失败,返回 0 。

    当且仅当 key 不存在时将 key 的值设为 value,并返回1;若给定的 key 已经存在,则 SETNX 不做任何动作,并返回0。

    综上所述,可以通过setnx的返回值来判断是否获取到锁,并且不用担心并发访问的问题,因为Redis是单线程的,所以如果返回1则获取到锁,返回0则没获取到。当业务操作执行完后,一定要释放锁,释放锁的逻辑很简单,就是把之前设置的key删除掉即可,这样下次又可以通过setnx该key获取到锁了。

    二、分布式锁实现

    我们已经知道可以通过Redis自带的函数setNX来实现分布式锁,具体实现步骤如下。

    我在一台CentOS7的linux虚拟机中安装了Redis服务,ip地址为:192.168.246.130,服务端口为:6379。

    下面是java通过redis实现分布式锁的例子:

    import redis.clients.jedis.Jedis;
    public class RedisLock {
    	//锁的key
    	private static final String key = "DistributedRedisLock";
    	private static Integer count = 0;
    	public static void main(String[] args) {
    		for(int i=0;i<1000;i++){
    			new Thread(new Runnable() {
    				@Override
    				public void run() {
    					//获取Redis连接
    					Jedis jedis = new Jedis("192.168.246.130", 6379);
    					try{
    						while(true){
    							//获取锁
    							if(jedis.setnx(key, Thread.currentThread().getName()) == 1){
    								try{
    									System.out.println("线程("+Thread.currentThread().getName()+")获取到锁,开始执行操作");
    									count++;
    									System.out.println(count);
    									break;
    								}finally{
    									System.out.println("操作执行完成,释放锁");
    									//操作执行完一定要释放锁,所以在finally块中执行
    									jedis.del(key);
    								}
    							}else{
    								//返回的不是1,说明已经有某个线程获取到了锁
    								try {
    									//等待100毫秒之后重试
    									Thread.sleep(100l);
    								} catch (InterruptedException e) {
    									e.printStackTrace();
    								}
    							}
    						}
    					}catch(Exception e){
    						e.printStackTrace();
    					}finally{
    						//释放Redis连接
    						jedis.disconnect();
    					}
    				}
    			}).start();
    		}
    	}
    }

     

    上述代码的输出结果为:

    线程(Thread-320)获取到锁,开始执行操作

    1

    操作执行完成,释放锁

    线程(Thread-463)获取到锁,开始执行操作

    2

    操作执行完成,释放锁

    线程(Thread-997)获取到锁,开始执行操作

    3

    操作执行完成,释放锁

    ...

    线程(Thread-409)获取到锁,开始执行操作

    998

    操作执行完成,释放锁

    线程(Thread-742)获取到锁,开始执行操作

    999

    操作执行完成,释放锁

    线程(Thread-286)获取到锁,开始执行操作

    1000

    操作执行完成,释放锁

    上述代码虽然是在单应用多线程情况下测试的,但即便是在分布式环境下多应用多线程去获取锁,结果依然是正确的。

    三、解决死锁问题

    之前的例子代码只是测试代码,只是为了说明原理,例子本身很简单,所以有一些考虑不周的地方。比如当获取到锁之后在业务操作执行过程中发生了环境问题导致断开了和Redis的连接,那就无法在finally块中释放锁,导致其他等待获取锁的线程无限等待下去,也就是发生了死锁现象。

    解决方式:

    可以在Redis中给锁设置一个过期时间,这样即便无法释放锁,锁也能在一段时间后自动释放。

    代码上只需要在获取到锁之后在try语句块中加入如下代码:

    jedis.expire(key, 10); //这里给锁设置10秒的过期时间

    更妥善的解决方式:

    第一个解决方式并不是很好,因为当业务操作处理时间很长,超过了设置的过期时间,那锁就自动释放了,然后再执行finally块中释放锁的操作时,这个锁可能已经被其他线程所持有,会导致把其他线程持有的锁给释放了,从而导致并发问题。所以更妥善一点的方式是在释放锁时判断一下锁是否已经过期,如果已经过期就不用再释放了。

    代码上把获取到锁之后的操作改为如下代码:

    long start = System.currentTimeMillis(); //获取起始时间毫秒数
    try{
      jedis.expire(key, 10);
      ...
    }finally{
      ...
      if(System.currentTimeMillis() < start+10*1000){
         //如果之前设置的锁还未过期,则释放掉
         jedis.del(key);
      }
    }

     

    展开全文
  • 各位小伙伴儿, 上篇我们介绍了Java中的7类锁, 现在还有一个重头戏, 那就是分布式锁, 我们接着上篇的标题,继续探索~ 8. 分布式锁 8.1 为什么需要分布式锁 首先我们先了解一下分布式锁的使用场景, 然后再来理解为什么...

    各位小伙伴儿, 上篇我们介绍了Java中的7类锁, 现在还有一个重头戏, 那就是分布式锁, 我们接着上篇的标题,继续探索~

    8. 分布式锁

    8.1 为什么需要分布式锁

    首先我们先了解一下分布式锁的使用场景, 然后再来理解为什么需要分布式锁, 那么我们举两个例子进行阐述:

    • 银行转账问题: A在上海,B在北京同时在建行转账给杭州C,A转账时,会修改C处服务器的表,B不能在此刻转账,同理,B转账时,A不能做处理,A,B的转账操作时同步,必须保证数据的一致性,这就需要分布式锁来进行处理.
    • 取任务问题: 某服务提供一组任务,A系统请求随机从任务组中获取一个任务;B系统请求随机从任务组中获取一个任务。 在理想的情况下,A从任务组中挑选一个任务,任务组删除该任务,B从剩下的的任务中再挑一个,任务组删除该任务。 同样的,在真实情况下,如果不做任何处理,可能会出现A和B挑中了同一个任务的情况。
    • 真实开发中, 集群模式下对某一个共享变量进行多线性同步访问:
      在这里插入图片描述
    1. 上图可以看到,变量A存在JVM1、JVM2、JVM3三个JVM内存中(这个变量A主要体现是在一个类中的一个成员变量,是一个有状态的对象,例如:UserController控制器中的一个整形类型的成员变量).
    2. 如果不加任何控制的话,变量A同时都会在JVM分配一块内存,三个请求发过来同时对这个变量操作,显然结果是不对的!即使不是同时发过来,三个请求分别操作三个不同JVM内存区域的数据,变量A之间不存在共享,也不具有可见性,处理的结果也是不对的!
      这种情况下就要应用分布式锁来解决了.

    8.2 为什么分布式系统中不能用普通锁呢?普通锁和分布式锁有什么区别吗?

    • 普通锁
    1. 单一系统找那个, 同一个应用程序是有同一个进程, 然后多个线程并发会造成数据安全问题, 他们是共享同一块内存的, 所以在内存某个地方做标记即可满足需求.
    2. 例如synchronized和volatile+cas一样对具体的代码做标记, 对应的就是在同一个内存区域作了同步的标记.
    • 分布式锁
    1. 分布式系统中, 最大的区别就是不同系统中的应用程序都在各自机器上不同的进程中处理的, 这里的线程不安全可以理解为多进程造成的数据安全问题, 他们不会共享同一台机器的同一块内存区域, 因此需要将标记存储在所有进程都能看到的地方.
    2. 例如zookeeper作分布式锁,就是将锁标记存储在多个进程共同看到的地方,redis作分布式锁,是将其标记公共内存,而不是某个进程分配的区域.

    8.3 分布式锁应该具备哪些条件

    在分析分布式锁的三种实现方式之前,先了解一下分布式锁应该具备哪些条件:

    • 在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行;
    • 高可用的获取锁与释放锁;
    • 高性能的获取锁与释放锁;
    • 具备可重入特性;
    • 具备锁失效机制,防止死锁;
    • 具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。

    8.4 分布式锁的三种实现方式

    • 目前几乎很多大型网站及应用都是分布式部署的, 分布式场景中的数据一致性问题一直是一个比较重要的话题.
      分布式的CAP理论告诉我们任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance), 最多只能同时满足两项.
    • 分布式锁, 是一种思想, 它的实现方式有很多种. 比如, 我们将沙滩当做分布式锁的组件, 那么它看起来应该是这样的:
    1. 加锁: 在沙滩上踩一脚,留下自己的脚印,就对应了加锁操作。其他进程或者线程,看到沙滩上已经有脚印,证明锁已被别人持有,则等待
    2. 解锁: 把脚印从沙滩上抹去,就是解锁的过程
    3. 锁超时: 为了避免死锁,我们可以设置一阵风,在单位时间后刮起,将脚印自动抹去
    • 因此应运而生了三种实现分布式锁的方式:
    1. 基于数据库实现分布式锁;
    2. 基于缓存(Redis等)实现分布式锁;
    3. 基于Zookeeper实现分布式锁;
      尽管有这三种方案,但是不同的业务也要根据自己的情况进行选型,他们之间没有最好只有更适合!

    8.4.1 数据库的分布式锁

    基于表记录实现分布式锁
    • 基于数据库的实现方式的核心思想是:
      在数据库中创建一个表, 表中包含方法名等字段, 并在方法名字段上创建唯一索引.
      想要执行某个方法, 就使用这个方法名向表中插入数据, 成功插入则获取锁, 执行完成后删除对应的行数据释放锁.
    1. 创建一个表
    DROP TABLE IF EXISTS `method_lock`;
    CREATE TABLE `method_lock` (
      `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
      `method_name` varchar(64) NOT NULL COMMENT '锁定的方法名',
      `desc` varchar(255) NOT NULL COMMENT '备注信息',
      `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
      PRIMARY KEY (`id`),
      UNIQUE KEY `uidx_method_name` (`method_name`) USING BTREE
    ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='锁定中的方法';
    

    在这里插入图片描述
    2. 想要执行某个方法, 就使用这个方法名向表中插入数据:

    INSERT INTO method_lock (method_name, desc) VALUES ('methodName', '测试的methodName');
    

    因为我们对method_name做了唯一性约束, 这里如果有多个请求同事提交到数据库的话, 数据库会保证只有一个操作可以成功, 那么我们就可以认为操作成功的那个现场恒获得了该方法的锁, 可以执行方法体内容.
    3. 成功插入则获取锁, 执行完成后删除对应的行数据释放锁

    delete from method_lock where method_name ='methodName';
    
    • 基于表记录实现分布式锁的特点
    1. 这种锁没有失效时间, 一旦释放锁的操作失败就会导致锁记录一直在数据库中, 其他线程无法获得锁. 这个缺陷也很好解决, 比如可以做一个定时任务去定时清理.
    2. 这种锁的可靠性依赖于数据库, 建议设置备库, 避免单点, 进一步提高可靠性.
    3. 这种锁是非阻塞的, 因为插入数据失败之后会直接报错, 想要获得锁就需要再次操作. 如果需要阻塞式的, 可以来个for循环或while循环, 直至INSERT成功再返回.
    4. 这种锁也是非可重入的, 因为同一个线程在没有释放锁之前无法再次获得锁, 因为数据库中已经存在同一份记录了. 想要实现可重入锁, 可以在数据库中添加一些字段, 比如获得锁的主机信息、线程信息等, 那么在再次获得锁的时候可以先查询数据, 如果当前的主机信息和线程信息等能被查到的话, 可以直接把锁分配给它.
    基于乐观锁实现分布式锁

    系统认为数据的更新在大多数情况下是不会产生冲突的,只在数据库更新操作提交的时候才对数据作冲突检测。如果检测的结果出现了与预期数据不一致的情况,则返回失败信息.

    乐观锁大多数是基于数据版本(version)的记录机制实现的. 即为数据增加一个版本标识.

    1. 在基于数据库表的版本解决方案中, 一般是通过为数据库表添加一个 “version”字段来实现读取出数据时, 将此版本号一同读出, 之后更新时, 对此版本号加1.
    2. 在更新过程中, 会对版本号进行比较, 如果是一致的, 没有发生改变, 则会成功执行本次操作; 如果版本号不一致, 则会更新失败.
    • 基于乐观锁的优点
      在检测数据冲突时并不依赖数据库本身的锁机制, 不会影响请求的性能, 当产生并发且并发量较小的时候只有少部分请求会失败.
    • 基于乐观锁的缺点
      需要对表的设计增加额外的字段, 增加了数据库的冗余, 另外, 当应用并发量高的时候, version值在频繁变化, 则会导致大量请求失败, 影响系统的可用性.

    综合数据库乐观锁的优缺点, 乐观锁比较适合并发量不高, 并且写操作不频繁的场景.

    基于悲观锁实现分布式锁

    除了通过增删操作数据库表中的记录来实现分布式锁, 我们还可以借助数据库中再带的锁来实现分布式锁.

    在查询语句后面增加For Update, 数据库会在查询过程中给数据库表增加悲观锁(也成排他锁), 当某条记录被加上悲观锁后, 其他线程也就无法再该行上增加悲观锁了.

    1. 悲观锁与乐观锁相反, 总是假设最坏的情况, 它认为数据的更新在大多数情况下是会产生冲突的.

    2. 在使用悲观锁的同时, 我们需要注意一下锁的级别. 搜索引擎的不同也会带了锁级别的不同.

      如果存储引擎是InnoDB, 在加锁的时候只有明确地指定主键(或索引)的才会执行行锁(只锁住被选取的数据), 否则Mysql将会执行表锁(将整个数据表单给锁住).

    • 使用悲观锁实现分布式锁特点
    1. 在悲观锁中, 每一次行数据的访问都是独占的, 只有当正在访问该行数据的请求事务提交以后, 其他请求才能依次访问该数据, 否则将阻塞等待锁的释放.
    2. 悲观锁可以严格保证数据访问的安全.
    3. 但是缺点也明显, 即每次请求都会额产生加锁的开销, 且未获取到锁的请求将会阻塞等待锁的释放, 在高并发环境下, 容易造成大量请求阻塞, 影响系统可用性,
    4. 悲观锁使用不当还可能产生死锁的情况

    8.4.2 Redis的分布式锁

    1) 单节点Redis分布式锁
    • 加锁
      加锁实际上就是在Redis中, 给Key键设置一个值, 为避免死锁, 并给定一个过期时间.
      Set lock_key random_value NX PX 5000
    1. random_value是客户端生成的唯一的字符串.
    2. NX 代表只在键不存在时, 才对键进行设置操作.
    3. PX 5000设置键的过期时间为5000毫秒
      这样, 如果上面的命令执行成功, 则证明客户端获取到了锁.
    • 解锁
      解锁的过程就是将Key键删除, 但也不能乱删, 不能说客户端1的请求将客户端2的锁给删掉.
      通过random_value的唯一标识来判别哪个客户端. 删除的时候先输入要删除的random_value, 然后判断当前random_value与先输入的是否相等, 是的话就删除Key, 解锁成功.
    • 单机模式Redis分布式锁优缺点:
      1. 实现比较容易, 如果是单机模式也容易满足需求.
      2. 但因为是单机单例单实例部署, 如果Redis服务宕机, 那么所有需求获取分布式锁的地方均无法获取锁, 将全部阻塞, 需要做好降级处理.
      3. 当锁过期后, 执行任务的进程还没有执行完, 但是锁因为自动过期已经解锁,可能被其它进程重新加锁, 这就造成多个进程同时获取到了锁, 这需要额外的方案来解决这种问题.
    2) 集群模式的Redis分布式锁 Redlock
    • Redlock算法是什么
      针对Redis集群架构,redis的作者antirez提出了Redlock算法,来实现集群架构下的分布式锁。
      在这里插入图片描述
      Redlock算法并不复杂,我们先简单描述一下,假设我们Redis分片下,有三个Master的节点,这三个Master,又各自有一个Slave,现在客户端想获取一把分布式锁:
      1. 记下开始获取锁的时间 startTime
      2. 按照A->B->C的顺序,依次向这三台Master发送获取锁的命令。客户端在等待每台Master回响应时,都有超时时间timeout。举个例子,客户端向A发送获取锁的命令,在等了timeout时间之后,都没收到响应,就会认为获取锁失败,继续尝试获取下一把锁
      3. 如果获取到超过半数的锁,也就是 3/2+1 = 2把锁,这时候还没完,要记下当前时间endTime
        计算拿到这些锁花费的时间 costTime = endTime - startTime,如果costTime小于锁的过期时间expireTime,则认为获取锁成功
      4. 如果获取不到超过一半的锁,或者拿到超过一半的锁时,计算出costTime>=expireTime,这两种情况下,都视为获取锁失败
      5. 如果获取锁失败,需要向全部Master节点,都发生释放锁的命令,也就是那段Lua脚本
    • Redlock优缺点:
      1. Redlock是Redis的作者antirez给出的集群模式的Redis分布式锁, 它基于N个完全独立的Redis节点.
      2. 部分节点宕机, 依然可以保证锁的可用性.
      3. 当某个节点宕机后, 又立即重启了, 可能会出现两个客户端同时持有同一把锁, 如果节点设置了持久化, 出现这种情况的几率会降低.
      4. 和单机模式锁相比, 实现难度要大些.
    3) 集群模式的Redis分布式锁 Redisson(基于Redlock)

    Redisson是一个基于Java编程框架netty进行扩展了的Redis.

    1. Redisson是架设在Redis基础上的一个Java驻内存数据网格, 可以理解为是一套开源框架.充分的利用了Redis键值数据库提供的一系列优势, 基于Java实用工具包中的常用接口, 为使用者提供了一系列具有分布式特性的常用工具类.
    2. 进一步简化了分布式环境中程序相互之间的协作.相对于Jedis而言, Redisson更强的是实现类分布式锁, 而且包含各种类型的锁.

    Redisson适用于: 分布式应用, 分布式缓存, 分布式会话管理, 分布式服务(任务, 延迟任务, 执行器), 分布式Redis客户端.
    目前操作Redisson有三种方式:

    1. 第一种:纯java操作,本文就是使用这种,所有的配置都写在一个 Class 里。
    2. 第二种:spring集成操作,编写一个 xml,配置一个bean,启动还需读取这个文件,一堆很原始的操作。使用这种 xml 配置我看着都烦,强烈不推荐。
    3. 第三种:文件方式配置,是把所有配置的参数放到配置文件声明,然后在 Class 中读取。

    我们先看一张Redisson实现Redis分布式锁的底层原理
    在这里插入图片描述

    • 加锁机制: 如果某个客户端要加锁, 它面对的是Redis Cluster集群, 首先会根据hash节点选择一台机器.
    • 锁互斥机制: 这个时候如果客户端2来尝试家锁, 发现myLock这个锁Key已经存在了, 在Mylock这个锁key的剩余时间内, 客户端2会进入一个while循环, 不停的尝试加锁.
    • watch dog自动延期机制: 客户端1一旦加锁成功, 就会启动一个watch dog看门够, 他是一个后台线程,会每隔10秒检查一下, 如果客户端1还持有锁key, 那么就会不断的延长锁key的生存时间.
    • 可重入加锁机制: 执行可重入锁,会对客户端1的加锁次数, 累加1.
    • 锁释放机制: 执行释放锁, 就会对和护短加锁次数减1. 如果发现锁此时是0, 就从Redis中删除这个key, 另外客户端2就可以尝试加锁了.

    8.4.3 Zookeeper分布式锁

    Zookeeper分布式锁的实现, 主要是因为Zookeeper有以下特点:

    • 维护了一个有层次的数据节点, 类似文件系统.
    • 树状数据节点: 临时节点, 持久节点, 临时有序节点(分布式锁实现基于的是临时有序节点), 持久有序节点.
    • Zookeepe可以和client客户端通过心跳的机制保持长连接, 如果客户端连接Zookeeper创建了一个临时节点, 那么客户端与Zookeeper断开连接后会自动删除.
    • Zookeeper保持了统一视图, 各服务对于状态信息获取满足一致性.

    Zookeeper的每一个节点, 都是一个天然的顺序发号器.

    在每一个节点下面创建子节点时,只要选择的创建类型是有序(EPHEMERAL_SEQUENTIAL 临时有序或者PERSISTENT_SEQUENTIAL 永久有序)类型,那么,新的子节点后面,会加上一个次序编号。这个次序编号,是上一个生成的次序编号加一.

    比如,创建一个用于发号的节点“/test/lock”,然后以他为父亲节点,可以在这个父节点下面创建相同前缀的子节点,假定相同的前缀为“/test/lock/seq-”,在创建子节点时,同时指明是有序类型。如果是第一个创建的子节点,那么生成的子节点为/test/lock/seq-0000000000,下一个节点则为/test/lock/seq-0000000001,依次类推,等等.

    在这里插入图片描述
    如何使用Zookeeper实现分布式锁呢?

    1) 排它锁

    排他锁,又称写锁或独占锁。如果事务T1对数据对象O1加上了排他锁,那么在整个加锁期间,只允许事务T1对O1进行读取或更新操作,其他任务事务都不能对这个数据对象进行任何操作,直到T1释放了排他锁。

    排他锁核心是保证当前有且仅有一个事务获得锁,并且锁释放之后,所有正在等待获取锁的事务都能够被通知到。

    Zookeeper 的强一致性特性,能够很好地保证在分布式高并发情况下节点的创建一定能够保证全局唯一性,即Zookeeper将会保证客户端无法重复创建一个已经存在的数据节点。可以利用Zookeeper这个特性,实现排他锁。

    • 定义锁:通过Zookeeper上的数据节点来表示一个锁
    • 获取锁:客户端通过调用 create 方法创建表示锁的临时节点,可以认为创建成功的客户端获得了锁,同时可以让没有获得锁的节点在该节点上注册Watcher监听,以便实时监听到lock节点的变更情况
    • 释放锁:以下两种情况都可以让锁释放
      当前获得锁的客户端发生宕机或异常,那么Zookeeper上这个临时节点就会被删除.
      正常执行完业务逻辑,客户端主动删除自己创建的临时节点.

    基于Zookeeper实现排他锁流程:
    在这里插入图片描述

    2) 共享锁

    共享锁,又称读锁。如果事务T1对数据对象O1加上了共享锁,那么当前事务只能对O1进行读取操作,其他事务也只能对这个数据对象加共享锁,直到该数据对象上的所有共享锁都被释放。

    共享锁与排他锁的区别在于,加了排他锁之后,数据对象只对当前事务可见,而加了共享锁之后,数据对象对所有事务都可见。

    • 定义锁:通过Zookeeper上的数据节点来表示一个锁,是一个类似于 /lockpath/[hostname]-请求类型-序号 的临时顺序节点
    • 获取锁:客户端通过调用 create 方法创建表示锁的临时顺序节点,如果是读请求,则创建 /lockpath/[hostname]-R-序号 节点,如果是写请求则创建 /lockpath/[hostname]-W-序号节点
    • 判断读写顺序:大概分为4个步骤
        1. 创建完节点后,获取 /lockpath 节点下的所有子节点,并对该节点注册子节点变更的Watcher监听
        2. 确定自己的节点序号在所有子节点中的顺序
        3.1 对于读请求:1. 如果没有比自己序号更小的子节点,或者比自己序号小的子节点都是读请求,那么表明自己已经成功获取到了共享锁,同时开始执行读取逻辑 2. 如果有比自己序号小的子节点有写请求,那么等待
        3.2 对于写请求,如果自己不是序号最小的节点,那么等待
        4. 接收到Watcher通知后,重复步骤1)
    • 释放锁:与排他锁逻辑一致.
      在这里插入图片描述
      基于Zookeeper实现共享锁流程
      在这里插入图片描述
    3) 羊群效应

    在实现共享锁的 “判断读写顺序” 的第1个步骤是:创建完节点后,获取 /lockpath 节点下的所有子节点,并对该节点注册子节点变更的Watcher监听。

    这样的话,任何一次客户端移除共享锁之后,Zookeeper将会发送子节点变更的Watcher通知给所有机器,系统中将有大量的 “Watcher通知” 和 “子节点列表获取” 这个操作重复执行,然后所有节点再判断自己是否是序号最小的节点(写请求)或者判断比自己序号小的子节点是否都是读请求(读请求),从而继续等待下一次通知。

    然而,这些重复操作很多都是 “无用的”,实际上每个锁竞争者只需要关注序号比自己小的那个节点是否存在即可。

    当集群规模比较大时,这些 “无用的” 操作不仅会对Zookeeper造成巨大的性能影响和网络冲击,更为严重的是,如果同一时间有多个客户端释放了共享锁,Zookeeper服务器就会在短时间内向其余客户端发送大量的事件通知–这就是所谓的 “羊群效应”。

    改进后的分布式锁实现:

    • 客户端调用 create 方法创建一个类似于 /lockpath/[hostname]-请求类型-序号 的临时顺序节点。

    • 客户端调用 getChildren 方法获取所有已经创建的子节点列表(这里不注册任何Watcher)。

    • 如果无法获取任何共享锁,那么调用 exist 来对比自己小的那个节点注册Watcher
          读请求:向比自己序号小的最后一个写请求节点注册Watcher监听
          写请求:向比自己序号小的最后一个节点注册Watcher监听

    • 等待Watcher监听,继续进入第二个步骤.

    Zookeeper羊群效应改进前后Watcher监听图:
    在这里插入图片描述

    展开全文

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 32,920
精华内容 13,168
关键字:

分布式锁