今天聊聊分布式锁 No.86

首先祝大家新年快乐,感谢大家过去一年的陪伴。大蕉在这里给大家拜年啦啦。新年快乐,我爱学习。

恭喜发财,红包拿来~

好了切入正题,一直在工作中会聊到很多锁的问题,今天跟大家一起闲聊一下,究竟什么是锁,为什么需要锁,以及分布式的情况下,怎么设计和实现锁。

什么是锁?

明·魏禧《大铁椎传》上是这样解释的:

锁:置于可启闭的器物上,以钥匙或暗码(如字码机构、时间机构、自动释放开关、磁性螺线管等)打开的扣件,例如:柄铁折叠环复,如锁上练,引之长丈许。

锁,就是要对一个可启闭的东西上,拥有者拥有着钥匙或者某些 Code , 用于打开的扣件。那么锁为什么要产生?为什么要用锁来将那个东西给加上锁,以便达到只有拥有者可以操作的效果呢?

历史上来看,锁几乎与私有制同时诞生。早在公元前3000年的中国仰韶文化遗址中,就留存有装在木结构框架建筑上的木锁。东汉时,中国铁制三簧锁的技术已具有相当高的水平。三簧锁前后沿用了1000多年。

那么在互联网,在软件中的锁是什么定义呢?在我看来,锁就是保证多线程在竞态条件下对共享资源操作的一致性。

怎么理解?

如果没有共享资源,那么锁并没有任何作用,每个业务每个线程都拥有自己的独占的资源,那么锁也就没有用武之地了。这些资源,任何其他业务其他线程都访问不了,那么这些资源对于本业务来说就是私有的,也就不需要加锁了。

那什么叫竞态条件呢?百科里是这样解释的:

竞态条件(race condition),从多进程间通信的角度来讲,是指两个或多个进程对共享的数据进行读或写的操作时,最终的结果取决于这些进程的执行顺序。

我们可以抓住三个关键字,多进程、共享数据、执行顺序。如果并没有多进程多线程,那么并不需要锁,因为不可能会出现竞态。如果所有的操作都是有序的,那么也不需要锁,因为顺序操作只要每个操作都是原子性的,那么基本不可能会出现竞态。

所以,锁的出现,是为了保证多线程在竞态条件下对共享资源操作的一致性。

经典传统应用环境下锁的使用机制是怎么样的?我们都知道数据库有很多种锁。乐观锁,悲观锁,排他锁,行锁,表锁... 诸如此类的定义。我们这里只稍微看乐观锁和悲观锁。

乐观锁:很乐观。认为大家的数据操作都是很守规矩的不会乱来,所以只在修改操作的时候会加锁。

悲观锁:很悲观。认为大家的数据操作都是不可估计而且可能带来严重影响的,所以在整个操作过程都会进行加锁。

当然,各种设计可能在这个层次之上会加上意向锁,意思就是你要获得乐观锁之前,要先获得意见乐观锁,意向排他锁与此类似。

也可能很变态,带上意向的意向锁。就好像,预约一下去预约去预约买车牌的预约。

传统的应用因为都是单机的,所以可以单起一个线程单独控制所有的操作即可,对数据进行锁定。很多的数据库都实现了相应的锁机制。

例如 Oracle ,根据保护的对象不同,Oracle实现的数据库锁可以分为以下几大类。

1、DML锁(data locks,数据锁),用于保护数据的完整性。

2、DDL锁(dictionary locks,字典锁),用于保护数据库对象的结构,如表、索引等的结构定义。

3、内部锁和闩(internal locks and latches),保护数据库的内部结构。

例如 MySQL ,不同的引擎支持的锁类型是不一样的,下面的表格可以一探究竟。至于乐观、悲观、意向乐观、意向悲观这些的设计跟 Oracle 如出一辙。

行锁

表锁

页锁

MyISAM

BDB

InnoDB

但是慢慢的,很多软件都运行在分布式的环境下,具体的套路可以看看我之前的文章。分布式架构的套路No.74

那为什么需要在分布式环境下使用锁呢?传统的应用在单机的情况下直接用一个统一的线程进行管控就可以了,但是在分布式环境下情况又不一样了。如果每个人都只持有自己的锁,对于其他人不可见,并不是全局唯一的锁,这样的锁是没有意义的。所以也就会有了分布式架构下的锁。

分布式锁有两层含义。第一层是在分布式的系统中用锁来保证业务的正确性。另外一层是用分布式的服务来保证锁的高可用性。

在分布式的环境中,分布式锁的实现方式大概有下面这么几种。

  1. 数据库。
  2. 缓存。
  3. 分布式一致性系统。

下面我们一一来聊他们的设计和实现。

数据库

现在 MySQL 和很多数据库都实现了分布式,但是也可以使用 MySQL 自己来实现分布式锁,实现方式是这样的。

1、在分布式操作之前,对数据库的定义了唯一键的表中插入一条数据。

2、操作之后,将这条数据删除掉。

3、启动一个定时 Job ,对已经过时的锁进行删除。

这样就能实现一个基于数据库的排他锁了。

缓存系统

缓存系统在实现的时候跟数据库的模式差不多,但是因为数据都是在缓存中,所以加锁和解锁都会比数据库快很多。

下面举例看看基于 Redis 的分布式锁实现。Redis 的分布式锁都是基于一个命令 -- SETNX,也就是 SET IF NOT EXIST,如果不存在就写入。从 Redis 2.6.12 版本开始,Redis 的 SET 命令直接直接设置 NX 和 EX 属性,NX 即附带了 SETNX 数据,key 存在就无法插入,EX 是过期属性,可以设置过期时间。这样一个命令就能原子的完成加锁和设置过期时间。

代码在这,自己看看吧。

pom文件是这样。

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
    <type>jar</type>
    <scope>compile</scope>
</dependency>

实现的代码是这样的。我就不讲解了,Code will talk 。

package lock;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import utils.Printer;

import java.util.Collections;
import java.util.ResourceBundle;

/**
 * @Author  大蕉
 * @Since   2018.02.13
 * @desc    基于redis的分布式锁实现
 */


public class RedisManager {
   public static JedisPool jedisPool;
   private static final String LOCK_SUCCESS = "OK";
   private static final String SET_IF_NOT_EXIST = "NX";
   private static final String SET_WITH_EXPIRE_TIME = "PX";
   private static final Long RELEASE_SUCCESS = 1L;


   /**
    * 
    * 过期时间设置
    * EX second :设置键的过期时间为 second 秒。 SET key value EX second 效果等同于 SETEX key second value 。
    * PX millisecond :设置键的过期时间为 millisecond 毫秒。 SET key value PX millisecond 效果等同于 PSETEX key millisecond value 。
    *
    * 执行条件设置
    * NX :只在键不存在时,才对键进行设置操作。 SET key value NX 效果等同于 SETNX key value 。
    * XX :只在键已经存在时,才对键进行设置操作。
    */


   static {

      //读取相关的配置
      ResourceBundle resourceBundle = ResourceBundle.getBundle("redis");
      int maxActive = Integer.parseInt(resourceBundle.getString("redis.pool.maxActive"));
      int maxIdle = Integer.parseInt(resourceBundle.getString("redis.pool.maxIdle"));
      int maxWait = Integer.parseInt(resourceBundle.getString("redis.pool.maxWait"));

      String ip = resourceBundle.getString("redis.ip");
      int port = Integer.parseInt(resourceBundle.getString("redis.port"));

      JedisPoolConfig config = new JedisPoolConfig();
      //设置最大连接数
      config.setMaxTotal(maxActive);
      //设置最大空闲数
      config.setMaxIdle(maxIdle);
      //设置超时时间
      config.setMaxWaitMillis(maxWait);
      //初始化连接池
      jedisPool = new JedisPool(config, ip, port);
   }


   public static boolean tryLock(String key,String value,int expireSecond){
      Jedis jedis = jedisPool.getResource();
      if(jedis == null){
         return false;
      }

      String result = jedis.set(key, value, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireSecond);

      if (LOCK_SUCCESS.equals(result)) {
         return true;
      }
      return false;

   }

   public static boolean releaseDistributedLock(String key,String value) {

      Jedis jedis = jedisPool.getResource();
      if(jedis == null){
         return false;
      }

      String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
      Object result = jedis.eval(script, Collections.singletonList(key), Collections.singletonList(value));

      if (RELEASE_SUCCESS.equals(result)) {
         return true;
      }
      return false;

   }


   public static void main(String[] args){
      Printer.println(tryLock("A","B",100));
      Printer.println(releaseDistributedLock("A","B"));
   }
}

除此之外,Redis 的作者还实现了一个分布式锁算法,叫Redlock,有兴趣的朋友自己 Google 。感兴趣的朋友多的话,我后面再聊聊这个算法的由来和因缘。

那么基于 Tair 的分布式锁是怎么实现的呢?

Tair 多了一个版本的概念,所以另外一种实现思路是用版本来控制锁。加锁的时候写一个默认的版本号,那么如果两次写入都指定了同一个版本的话,服务端会直接报错导致加锁失败。

分布式一致性系统

分布式一致性的系统,现在最流行的应该局势 Zookeeper 了。Zookeeper 实现了类 Paxos 的设计。用 Zookeeper 是使用新增子节点的模式来进行加锁。

比如 B 要对数据 A 进行加锁,可以这样操作。 create /locks/A "B"

其他节点在对 Zookeeper 进行加锁的时候,因为目录已经存在,会直接报错。解锁的时候直接 delete /locks/A ,这样就好了。

Zookeeper 是怎么实现分布式一致性的呢?最最主要的设计就是 Zookeeper 实现了 leader 选举制以及 follower 转发制。follower 在接收到请求的时候,会直接转发给 leader,由 leader 进行数据的统一处理。

好了,今天的分布式锁就聊到这里啦,大家有什么想聊的可以留言评论告诉我,平时也会分享一些小玩意小感想,一起到我的小蜜圈来玩耍吧。新年快乐么么哒 ? 。

原文发布于微信公众号 - 一名叫大蕉的程序员(DaBananaTalk)

原文发表时间:2018-02-17

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏工作积累

回答:这几天找工作遇到的面试题目

https://laravel-china.org/articles/9983/the-interview-questions-we-met-in-the-la...

3.1K10
来自专栏大数据杂谈

Python 爬虫实践:《战狼2》豆瓣影评分析

2455
来自专栏GopherCoder

专栏:013:我要你知道实时票房.

1483
来自专栏程序员的SOD蜜

ORM查询语言(OQL)简介--概念篇

相关文章内容索引: ORM查询语言(OQL)简介--概念篇 ORM查询语言(OQL)简介--实例篇 ORM查询语言(OQL)简介--高级篇:脱胎换骨 ORM查...

27510
来自专栏北京马哥教育

Python 爬虫实践:《战狼2》豆瓣影评分析

来源:hang segmentfault.com/a/1190000010473819 简介 刚接触python不久,做一个小项目来练练手。前几天看了《战狼2》...

4944
来自专栏芋道源码1024

数据库分库分表中间件 Sharding-JDBC 源码分析 —— 分布式主键

本文主要基于 Sharding-JDBC 1.5.0 正式版 1. 概述 2.KeyGenerator 2.1 DefaultKeyGenerator 2.2...

48514
来自专栏haifeiWu与他朋友们的专栏

复杂业务下向Mysql导入30万条数据代码优化的踩坑记录

从毕业到现在第一次接触到超过30万条数据导入MySQL的场景(有点low),就是在顺丰公司接入我司EMM产品时需要将AD中的员工数据导入MySQL中,因此楼主负...

1634
来自专栏noteless

2.计算机组成-数字逻辑电路 门电路与半加器 异或运算半加器 全加器组成 全加器结构 反馈电路 振荡器 存储 D T 触发器 循环移位 计数器 寄存器 传输门电路 译码器 晶体管

所以想要准确的保存一个比特,你需要保持住D的值,持续经过CP从0~1然后再到0的过程

4393
来自专栏圆方圆学院精选

【刘文彬】EOS技术研究:合约与数据库交互

原文链接:醒者呆的博客园,https://www.cnblogs.com/Evsward/p/multi_index.html

1252
来自专栏更流畅、简洁的软件开发方式

【自然框架】用CMS的栏目举例,聊一聊从“一层”到“三层”的变化

  做CMS最基本的一个功能就是做一个栏目导航,如果这个导航想做成动态的(即需要从数据库里提取数据)那么要如何实现呢? 简单的方法——DataTable   ...

2269

扫码关注云+社区

领取腾讯云代金券