分布式锁三种实现方式:

  1. 基于数据库实现分布式锁;

  2. 基于缓存(Redis等)实现分布式锁;

  3. 基于Zookeeper实现分布式锁;

基于数据库实现分布式锁

悲观锁

利用select … where … for update 排他锁

注意: 其他附加功能与实现一基本一致,这里需要注意的是“where name=lock
”,name字段必须要走索引,否则会锁表。有些情况下,比如表不大,mysql优化器会不走这个索引,导致锁表问题。

乐观锁

所谓乐观锁与前边最大区别在于基于CAS思想,是不具有互斥性,不会产生锁等待而消耗资源,操作过程中认为不存在并发冲突,只有update version失败后才能觉察到。我们的抢购、秒杀就是用了这种实现以防止超卖。

通过增加递增的版本号字段实现乐观锁

基于缓存(Redis等)实现分布式锁

使用命令介绍

(1)SETNX

SETNX key val:当且仅当key不存在时,set一个key为val的字符串,返回1;若key存在,则什么都不做,返回0。

(2)expire  
expire key timeout:为key设置一个超时时间,单位为second,超过这个时间锁会自动释放,避免死锁。

(3)delete  
delete key:删除key
在使用Redis实现分布式锁的时候,主要就会使用到这三个命令。

实现思想:

(1)获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。 
(2)获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。 
(3)释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放。

分布式锁的简单实现代码

 

 /**
  * 分布式锁的简单实现代码  4  */
 public class DistributedLock {

     private final JedisPool jedisPool;

     public DistributedLock(JedisPool jedisPool) {
         this.jedisPool = jedisPool;
     }

     /**
      * 加锁
      * @param lockName       锁的key
      * @param acquireTimeout 获取超时时间
      * @param timeout        锁的超时时间
      * @return 锁标识
      */
     public String lockWithTimeout(String lockName, long acquireTimeout, long timeout) {
         Jedis conn = null;
         String retIdentifier = null;
         try {
             // 获取连接
             conn = jedisPool.getResource();
             // 随机生成一个value
             String identifier = UUID.randomUUID().toString();
             // 锁名,即key值
             String lockKey = "lock:" + lockName;
             // 超时时间,上锁后超过此时间则自动释放锁
             int lockExpire = (int) (timeout / 1000);

             // 获取锁的超时时间,超过这个时间则放弃获取锁
             long end = System.currentTimeMillis() + acquireTimeout;
             while (System.currentTimeMillis() < end) {
                 if (conn.setnx(lockKey, identifier) == 1) {
                     conn.expire(lockKey, lockExpire);
                     // 返回value值,用于释放锁时间确认
                     retIdentifier = identifier;
                     return retIdentifier;
                 }
                 // 返回-1代表key没有设置超时时间,为key设置一个超时时间
                 if (conn.ttl(lockKey) == -1) {
                     conn.expire(lockKey, lockExpire);
                 }

                 try {
                     Thread.sleep(10);
                 } catch (InterruptedException e) {
                     Thread.currentThread().interrupt();
                 }
             }
         } catch (JedisException e) {
             e.printStackTrace();
         } finally {
             if (conn != null) {
                 conn.close();
             }
         }
         return retIdentifier;
     }
     /**
      * 释放锁
      * @param lockName   锁的key
      * @param identifier 释放锁的标识
      * @return
      */
     public boolean releaseLock(String lockName, String identifier) {
         Jedis conn = null;
         String lockKey = "lock:" + lockName;
         boolean retFlag = false;
         try {
             conn = jedisPool.getResource();
             while (true) {
                 // 监视lock,准备开始事务
                 conn.watch(lockKey);
                 // 通过前面返回的value值判断是不是该锁,若是该锁,则删除,释放锁
                 if (identifier.equals(conn.get(lockKey))) {
                     Transaction transaction = conn.multi();
                     transaction.del(lockKey);
                     List<Object> results = transaction.exec();
                     if (results == null) {
                         continue;
                     }
                     retFlag = true;
                 }
                 conn.unwatch();
                 break;
             }
         } catch (JedisException e) {
             e.printStackTrace();
         } finally {
             if (conn != null) {
                 conn.close();
             }
         }
         return retFlag;
     }
 }

测试实现的分布式锁

例子中使用50个线程模拟秒杀一个商品,使用–运算符来实现商品减少,从结果有序性就可以看出是否为加锁状态。

模拟秒杀服务,在其中配置了jedis线程池,在初始化的时候传给分布式锁,供其使用。

    public class Service {

        private static JedisPool pool = null;

        private DistributedLock lock = new DistributedLock(pool);

        int n = 500;

        static {
            JedisPoolConfig config = new JedisPoolConfig();
            // 设置最大连接数
            config.setMaxTotal(200);
            // 设置最大空闲数
            config.setMaxIdle(8);
            // 设置最大等待时间
            config.setMaxWaitMillis(1000 * 100);
            // 在borrow一个jedis实例时,是否需要验证,若为true,则所有jedis实例均是可用的
            config.setTestOnBorrow(true);
            pool = new JedisPool(config, "127.0.0.1", 6379, 3000);
        }

        public void seckill() {
            // 返回锁的value值,供释放锁时候进行判断
            String identifier = lock.lockWithTimeout("resource", 5000, 1000);
            System.out.println(Thread.currentThread().getName() + "获得了锁");
            System.out.println(--n);
            lock.releaseLock("resource", identifier);
        }
    }

模拟线程进行秒杀服务;

    public class ThreadA extends Thread {
        private Service service;

        public ThreadA(Service service) {
            this.service = service;
        }

        @Override
        public void run() {
            service.seckill();
        }
    }

    public class Test {
        public static void main(String[] args) {
            Service service = new Service();
            for (int i = 0; i < 50; i++) {
                ThreadA threadA = new ThreadA(service);
                threadA.start();
            }
        }
    }

结果如下,结果为有序的:

若注释掉使用锁的部分:

    public void seckill() {
        // 返回锁的value值,供释放锁时候进行判断
        //String indentifier = lock.lockWithTimeout("resource", 5000, 1000);
        System.out.println(Thread.currentThread().getName() + "获得了锁");
        System.out.println(--n);
        //lock.releaseLock("resource", indentifier);
    }

从结果可以看出,有一些是异步进行的:

三  基于Zookeeper实现分布式锁

ZooKeeper是一个为分布式应用提供一致性服务的开源组件,它内部是一个分层的文件系统目录树结构,规定同一个目录下只能有一个唯一文件名。基于ZooKeeper实现分布式锁的步骤如下:

(1)创建一个目录mylock; 
(2)线程A想获取锁就在mylock目录下创建临时顺序节点; 
(3)获取mylock目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁; 
(4)线程B获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点; 
(5)线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。
这里推荐一个Apache的开源库Curator,它是一个ZooKeeper客户端,Curator提供的 InterProcessMutex 是分布式锁的实现,acquire方法用于获取锁,release方法用于释放锁。

实现源码如下:

    import lombok.extern.slf4j.Slf4j;
    import org.apache.commons.lang.StringUtils;
    import org.apache.curator.framework.CuratorFramework;
    import org.apache.curator.framework.CuratorFrameworkFactory;
    import org.apache.curator.retry.RetryNTimes;
    import org.apache.zookeeper.CreateMode;
    import org.apache.zookeeper.data.Stat;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.context.annotation.Bean;
    import org.springframework.stereotype.Component;

    /**
     * 分布式锁Zookeeper实现
     *
     */
    @Slf4j
    @Component
    public class ZkLock implements DistributionLock {
    private String zkAddress = "zk_adress";
        private static final String root = "package root";
        private CuratorFramework zkClient;

        private final String LOCK_PREFIX = "/lock_";

        @Bean
        public DistributionLock initZkLock() {
            if (StringUtils.isBlank(root)) {
                throw new RuntimeException("zookeeper 'root' can't be null");
            }
            zkClient = CuratorFrameworkFactory
                    .builder()
                    .connectString(zkAddress)
                    .retryPolicy(new RetryNTimes(2000, 20000))
                    .namespace(root)
                    .build();
            zkClient.start();
            return this;
        }

        public boolean tryLock(String lockName) {
            lockName = LOCK_PREFIX+lockName;
            boolean locked = true;
            try {
                Stat stat = zkClient.checkExists().forPath(lockName);
                if (stat == null) {
                    log.info("tryLock:{}", lockName);
                    stat = zkClient.checkExists().forPath(lockName);
                    if (stat == null) {
                        zkClient
                                .create()
                                .creatingParentsIfNeeded()
                                .withMode(CreateMode.EPHEMERAL)
                                .forPath(lockName, "1".getBytes());
                    } else {
                        log.warn("double-check stat.version:{}", stat.getAversion());
                        locked = false;
                    }
                } else {
                    log.warn("check stat.version:{}", stat.getAversion());
                    locked = false;
                }
            } catch (Exception e) {
                locked = false;
            }
            return locked;
        }

        public boolean tryLock(String key, long timeout) {
            return false;
        }

        public void release(String lockName) {
            lockName = LOCK_PREFIX+lockName;
            try {
                zkClient
                        .delete()
                        .guaranteed()
                        .deletingChildrenIfNeeded()
                        .forPath(lockName);
                log.info("release:{}", lockName);
            } catch (Exception e) {
                log.error("删除", e);
            }
        }

        public void setZkAddress(String zkAddress) {
            this.zkAddress = zkAddress;
        }
    }

优点:具备高可用、可重入、阻塞锁特性,可解决失效死锁问题。
缺点:因为需要频繁的创建和删除节点,性能上不如Redis方式。

总结

数据库分布式锁实现

缺点:
1.db操作性能较差,并且有锁表的风险  
2.非阻塞操作失败后,需要轮询,占用cpu资源;  
3.长时间不commit或者长时间轮询,可能会占用较多连接资源

Redis(缓存)分布式锁实现

缺点:
1.锁删除失败 过期时间不好控制  
2.非阻塞,操作失败后,需要轮询,占用cpu资源;

ZK分布式锁实现

缺点:性能不如redis实现,主要原因是写操作(获取锁释放锁)都需要在Leader上执行,然后同步到follower。
总之:ZooKeeper有较好的性能和可靠性。

从理解的难易程度角度(从低到高)数据库 > 缓存 > Zookeeper
从实现的复杂性角度(从低到高)Zookeeper >= 缓存 > 数据库
从性能角度(从高到低)缓存 > Zookeeper >= 数据库
从可靠性角度(从高到低)Zookeeper > 缓存 > 数据库

使用数据库、Redis、ZK分别实现分布式锁!的更多相关文章

  1. 一般实现分布式锁都有哪些方式?使用redis如何设计分布式锁?使用zk来设计分布式锁可以吗?这两种分布式锁的实现方式哪种效率比较高?

    #(1)redis分布式锁 官方叫做RedLock算法,是redis官方支持的分布式锁算法. 这个分布式锁有3个重要的考量点,互斥(只能有一个客户端获取锁),不能死锁,容错(大部分redis节点创建了 ...

  2. 【连载】redis库存操作,分布式锁的四种实现方式[一]--基于zookeeper实现分布式锁

    一.背景 在电商系统中,库存的概念一定是有的,例如配一些商品的库存,做商品秒杀活动等,而由于库存操作频繁且要求原子性操作,所以绝大多数电商系统都用Redis来实现库存的加减,最近公司项目做架构升级,以 ...

  3. Redis、Zookeeper实现分布式锁——原理与实践

    Redis与分布式锁的问题已经是老生常谈了,本文尝试总结一些Redis.Zookeeper实现分布式锁的常用方案,并提供一些比较好的实践思路(基于Java).不足之处,欢迎探讨. Redis分布式锁 ...

  4. 使用Redis SETNX 命令实现分布式锁

    基于setnx和getset http://blog.csdn.net/lihao21/article/details/49104695 使用Redis的 SETNX 命令可以实现分布式锁,下文介绍其 ...

  5. Redis 上实现的分布式锁

    转载Redis 上实现的分布式锁 由于近排很忙,忙各种事情,还有工作上的项目,已经超过一个月没写博客了,确实有点惭愧啊,没能每天或者至少每周坚持写一篇博客.这一个月里面接触到很多新知识,同时也遇到很多 ...

  6. 在 Redis 上实现的分布式锁

    由于近排很忙,忙各种事情,还有工作上的项目,已经超过一个月没写博客了,确实有点惭愧啊,没能每天或者至少每周坚持写一篇博客.这一个月里面接触到很多新知识,同时也遇到很多技术上的难点,在这我将对每一个有用 ...

  7. Redis整合Spring实现分布式锁

    spring把专门的数据操作独立封装在spring-data系列中,spring-data-redis是对Redis的封装 <dependencies> <!-- 添加spring- ...

  8. 使用Redis SETNX 命令实现分布式锁(转载)

    使用Redis的 SETNX 命令可以实现分布式锁,下文介绍其实现方法. SETNX命令简介 命令格式 SETNX key value 将 key 的值设为 value,当且仅当 key 不存在. 若 ...

  9. 基于 Redis 实现简单的分布式锁

    摘要 分布式锁在很多应用场景下是非常有效的手段,比如当运行在多个机器上的不同进程需要访问同一个竞争资源的时候,那么就会涉及到进程对资源的加锁和释放,这样才能保证数据的安全访问.分布式锁实现的方案有很多 ...

随机推荐

  1. 利用redis+AOP简单处理MQ冥等问题

    思路: 1.利用redis内部的串行执行特性,使用getandset()处理分布式问题; 2.注解提供入参选择,通过数据抽取后计算MD5值,实现业务性值的冥等: 代码区: 1.注解 1 /** 2 * ...

  2. 【BZOJ2337】XOR和路径(高斯消元)

    题目链接 大意 给出\(N\)个点,\(M\)条边的一张图,其中每条边都有一个非负整数边权. 一个人从1号点出发,在与该点相连的边中等概率的选择一条游走,直到走到\(N\)号点. 问:将这条路径上的边 ...

  3. LeetCode随缘刷题之转化成小写字母

    这道题应该是最简单的一道题了把,简直在侮辱我. package leetcode.day_12_12; /** * 709. 转换成小写字母 * 给你一个字符串 s ,将该字符串中的大写字母转换成相同 ...

  4. 通过loganalyzer展示数据库中的日志

    一.安装mysql # yum -y install mariadb-server # systemctl enable --now mariadb && systemctl stat ...

  5. Flask中本地栈的使用

    4种上下文变量 承接上一篇内容.当一个请求到来时,除了request被封装成全局变量之外,还有三个变量也是同样被封装成全局变量,那就是current_app.g.session.上面4个变量之所以能够 ...

  6. suse 12 部署chrony时间同步服务器

    文章目录 1.ntp和chrony的区别 1.1.关于chrony 1.2.chronyd的优势 2.环境介绍 3.部署chrony 4.配置chrony 4.1.配置文件解析 4.2.查看chron ...

  7. 使用Hot Chocolate和.NET 6构建GraphQL应用(9) —— 实现Mutate更新数据

    系列导航 使用Hot Chocolate和.NET 6构建GraphQL应用文章索引 需求 在上一篇文章中,我们演示了如何使用Hot Chocolate进行GraphQL的Mutate新增数据,这篇文 ...

  8. WPF使用MVVM(三)-事件转命令

    WPF使用MVVM(三)-事件转命令 上一节介绍了WPF中的命令,可是仅仅介绍的是WPF框架给我们提供的点击命令,也就是用Command属性来绑定一个命令,用来响应按钮的点击行为!显然这是不够的,界面 ...

  9. RESTful风格了解

    最近在学习springboot,一直听到一个词叫RESTful风格,今天找了一下书了解了一番.spring mvc除了支持json数据交互外,还支持RESTful风格 RESTful也称为REST(r ...

  10. gradle , maven , ant , ivy , grant之间的区别

    java项目构建工具 gradle Gradle是一个基于Apache Ant和Apache Maven概念的项目自动化构建开源工具. 它抛弃了基于XML的各种繁琐配置.它使用一种基于Groovy的特 ...