• 分布式锁


    以下内容纯粹是个人在闲聊没事时凭自己的理解总结出来的,全部为手敲,可能在内容方面和网上的一些博客不一样,没有那些博主总结的到位,这里只是为了一个记录和总结。

    1、概念

    一个方法或者一段代码,在分布式情况,同一时间内只能被一个台机器执行。

    2、特征

    1. 锁的有效释放,防止死锁;
    2. 可重入锁;
    3. 高可用性;
    4. 高性能获取锁和释放锁;
    5. 非阻塞锁;

    3、实现方案

    3.1 基于数据库

    在数据库中创建一个表,字段大概为:id,methodName(设置唯一字段),version。

    利用数据库的字段唯一特性,获取到锁后再数据库中创建一条记录,如果创建失败,则为获取锁失败,可以休息一段时间再次尝试获取锁。

    问题:

    1.锁的释放,如果程序在释放锁之前,宕机 则一直会占用锁   解决办法:可以写一个定时任务,定时清除数据库中数据,定时器时间是个问题
    2.可重入锁:可以利用version字段设置一个字段,判断试过当前线程持有的version和数据库中version一致,则可以继续使用当前锁
    3.高可用:数据库要搭建高可用集群,如果单节点数据库,则数据库挂了就。。。。。
    

    3.2 基于Redis

    利用Redis的setNx命令,set if not exists 如果不存在,执行setnx命令,如果存在,则什么也不做,直接返回。

    setNx(key,value,time,timeunit);

    key:锁的唯一key

    value:这里值可以为一个uuid,每一个客户端都有一个唯一的uuid,然后通过比较判断可以实现可重入锁

    time:过期时间,如果程序宕机,则会在超时后自动释放锁,防止死锁的出现

    timeunit:时间单位

    问题

    1、如果此处设置的时间过短,那么程序还没有执行完,redis就自动释放了锁,会引发并发问题。解决方法:可以设置一个线程,去个锁"续命"
    2、高可用:redis服务器必须是高可用的集群
    3、setNx中的value要唯一,在释放锁时要进行判断,必须是自己创建的锁才可以释放
    

    在工作中,我们一般使用Redisson实现分布式锁。

    3.3 基于zookeeper

    zookeeper节点的介绍:

    1. 持久节点
    2. 持久有序节点
    3. 临时节点
    4. 临时有序节点

    基于zookeeper的分布式锁正是利用了临时有序节点的特征实现的。过程大概如下:

    1. 创建一个持久化节点ZKLock,
    2. Client1获取锁在ZKLock节点下创建一个临时有序节点Lock01,然后去ZKLock节点下比较所有的节点,判断自己是不是最小的,如果是最小的则获取锁成功。
    3. Client2这时再去获取锁时在ZKLock节点下创建一个临时有序节点Lock02,然后判断Lock02是不是最小节点,发现自己不是最小节点,则向它的前一个节点也就是client1注册Watcher事件,这就意味着Client2获取锁失败,进入等待状态
    4. Client3进来时,先创建一个Lock03节点,判断Lock03是不是当前最小节点,显然Lock03并不是最小的锁,则向Client2注册Watcher事件,并进入等待状态
    5. client1执行完成后,主动释放锁
    6. client1在主动释放锁之前宕机了,则根据临时节点的特性,zk会自动删除临时节点Lock1

    4、Redisson

    摘自官网一段话:

    Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。其中包括(BitSet, Set, Multimap, SortedSet, Map, List, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, AtomicLong, CountDownLatch, Publish / Subscribe, Bloom filter, Remote service, Spring cache, Executor service, Live Object service, Scheduler service) Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。

    这里我们只简单介绍一下分布式锁,它为我们提供了可重入锁(ReentrantLock),公平锁(Fair Lock),读写锁(ReadWriteLock),信号量(Semaphore)等等。

    简单使用方法,这里以可重入锁为例,其他可以参考官网

    RLock lock = redisson.getLock("anyLock");
    // 最常见的使用方法
    lock.lock();
    / 无需调用unlock方法手动解锁
    lock.lock(10, TimeUnit.SECONDS);
    
    // 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
    boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
    if (res) {
       try {
         ...
       } finally {
           lock.unlock();
       }
    }
    

    同时还提供了支持异步的方法:

    RLock lock = redisson.getLock("anyLock");
    lock.lockAsync();
    lock.lockAsync(10, TimeUnit.SECONDS);
    Future<Boolean> res = lock.tryLockAsync(100, 10, TimeUnit.SECONDS);
    

    Redisson官网: https://github.com/redisson/redisson

    中文文档: https://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95

    5、代码分析

    import org.redisson.Redisson;
    import org.redisson.api.RLock;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import java.util.UUID;
    import java.util.concurrent.TimeUnit;
    
    @RestController
    public class InitRedisClockController {
    
        @Autowired
        private RedisTemplate redisTemplate;
    
        @Autowired
        private Redisson redisson;
    
        @GetMapping("/initRedis")
        public String initRedisClock(){
            redisTemplate.opsForValue().set("redis_lock",100);
            return "OK";
        }
    
        @GetMapping("/deductLock")
        public String deductLock(){
            //添加synchronized,使用jmeter压测后发现会有超买的现象,这种在单台的服务器下没有问题,在分布式下有问题
            synchronized (this){
                int value = Integer.parseInt(redisTemplate.opsForValue().get("redis_lock").toString());
                if(value>0){
                    int leastValue = value -1;
                    redisTemplate.opsForValue().set("redis_lock",leastValue);
                    System.out.println("秒杀成功,剩余库存:"+leastValue);
                }else{
                    System.out.println("秒杀失败,库存不足");
                }
                return "end";
            }
    
        }
        @GetMapping("/deductLock2")
        public String deductLock2(){
            //使用redis的setnx命令,添加一个分布式锁,并设置一个锁的失效时间,防止死锁的问题,超过时间后redis自动释放该锁
            //这个写有一个问题,就是在高并发下,A线程加的锁会被B线程给释放,
            //此代码问题:A线程获得锁后  执行程序需要15秒,在10秒后  redis会自动释放锁
            //   此时 B线程就会获得锁,然后继续执行,这个A线程完成了任务,则回去释放锁,此时C线程就会进行,
            //在高并发下这种问题是致命的,会导致所有的程序错乱
            //这里可以把锁的超时时间设置长一点,但是,时间多长合适呢?
            try {
                Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent("fbs_lock", "wangxin",10, TimeUnit.SECONDS);
                if(!aBoolean){
                    System.out.println("没有获取到锁");
                    return "error";
                }
                int value = Integer.parseInt(redisTemplate.opsForValue().get("redis_lock").toString());
                if(value>0){
                    int leastValue = value -1;
                    redisTemplate.opsForValue().set("redis_lock",leastValue);
                    System.out.println("秒杀成功,剩余库存:"+leastValue);
                }else{
                    System.out.println("秒杀失败,库存不足");
                }
                return "end";
            } finally {
                redisTemplate.delete("fbs_lock");
            }
        }
    
        @GetMapping("/deductLock3")
        public String deductLock3(){
            //添加一个uuid作为唯一标识,判断锁只能由自己释放,不能被其他线程释放
            //此代码问题:A线程获得锁后  执行程序需要15秒,在10秒后  redis会自动释放锁
            //   此时 B线程就会获得锁,但是A线程还没有执行完毕,所以这样还是会在并发执行。
            String uuid = UUID.randomUUID().toString();
            try {
                Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent("fbs_lock", uuid,10, TimeUnit.SECONDS);
                if(!aBoolean){
                    System.out.println("没有获取到锁");
                    return "error";
                }
                int value = Integer.parseInt(redisTemplate.opsForValue().get("redis_lock").toString());
                if(value>0){
                    int leastValue = value -1;
                    redisTemplate.opsForValue().set("redis_lock",leastValue);
                    System.out.println("秒杀成功,剩余库存:"+leastValue);
                }else{
                    System.out.println("秒杀失败,库存不足");
                }
                return "end";
            } finally {
                //判断自己的锁只能自己去释放,其他线程不能释放
                if(uuid.equals(redisTemplate.opsForValue().get("fbs_lock"))){
                    redisTemplate.delete("fbs_lock");
                }
            }
        }
        @GetMapping("/deductLock4")
        public String deductLock4(){
           //针对上面的问题,使用Redisson解决
            String uuid = UUID.randomUUID().toString();
            RLock lock = redisson.getLock("fbs_lock");
            try {
                lock.lock();
                int value = Integer.parseInt(redisTemplate.opsForValue().get("redis_lock").toString());
                if(value>0){
                    int leastValue = value -1;
                    redisTemplate.opsForValue().set("redis_lock",leastValue);
                    System.out.println("秒杀成功,剩余库存:"+leastValue);
                }else{
                    System.out.println("秒杀失败,库存不足");
                }
                return "end";
            } finally {
                lock.unlock();
            }
        }
    
    }
    
  • 相关阅读:
    对Spring Boot 及Mybatis简单应用
    云时代架构读后感(十)
    云时代架构读后感(九)
    云时代架构读后感(八)
    云时代架构读后感(七)
    云时代架构读后感(六)
    关于mysql,sqlserverl,与oracle数据库连接总结
    云时代架构读后感(五)
    javaweb实现添加课程
    javaweb入门(使用SQLserver2008 R2数据库)
  • 原文地址:https://www.cnblogs.com/bky-wangxin/p/13351144.html
Copyright © 2020-2023  润新知