分布式锁

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

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

}
posted @ 2020-01-12 12:31  wxzj  阅读(96)  评论(0编辑  收藏  举报