文章目录
  1. 1. 分布式锁应用场景分析
    1. 1.1. 概念
    2. 1.2. 场景
      1. 1.2.1. 账户操作
      2. 1.2.2. 资源池获取任务
      3. 1.2.3. 公有账户给私有账户充值
      4. 1.2.4. 星币扣除
    3. 1.3. 业内解决方案
      1. 1.3.1. 基于Mysql实现分布式锁
      2. 1.3.2. 基于redis实现分布式锁(redis单进程单线程特性)
      3. 1.3.3. 基于zk实现分布式锁
    4. 1.4. 分布式锁设计目标

分布式锁应用场景分析

概念

Java线程中的锁,基于Java的内存模型,每个线程有自己的内存空间,多线程锁是存在一个JVM之中的,如果操作的数据不在一个JVM中,多线程中锁就失效了,这种情况下分布式锁就诞生了,即多个Java实例、甚至不一定是Java程序、或多个系统需要操作同一个副本数据的时候,需要一个指挥交通的人指定操作的先后顺序,这就是分布式锁的概念。

在传统的基于数据库的架构中,对于数据的抢占问题往往是通过数据库事务(ACID)来保证的。在分布式环境中,出于对性能以及一致性敏感度的要求,使得分布式锁成为了一种比较常见而高效的解决方案。

场景

账户操作

资源池获取任务

公有账户给私有账户充值

星币扣除

……

业内解决方案

基于Mysql实现分布式锁

  • MYSQL隔离性-唯一索引

    1
    2
    3
    4
    5
    6
    7
    8
    CREATE TABLE `t_distributed_lock` (
    `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
    `name` varchar(64) NOT NULL DEFAULT '' COMMENT '锁名',
    `desc` varchar(512) NOT NULL DEFAULT '备注信息',
    `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '保存数据时间,自动生成',
    PRIMARY KEY (`id`),
    UNIQUE KEY `uidx_name` (`name`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='锁数据';

(1) 加锁

1
2
3
4
//数据库中的每一条记录就是一把锁,利用的mysql唯一索引的排他性
lock(name,desc){
insert into t_distributed_lock(`name`,`desc`) values (#{name},#{desc});
}

(2) 解锁

1
2
3
unlock(name){
delete from t_distributed_lock where name = #{name}
}
  • 重入锁
  • 可靠性
  • 高性能
  • 阻塞锁
  • 利用 select … where … for update 排他锁(悲观锁)

    悲观锁–每次去拿数据的时候,都以为数据是被人改过的,所以每次都要加锁

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    boolean lock(){
    connection.setAutoCommit(false)
    while(true){
    try{
    result = select ... from t_distributed_lock where name=lock for update;
    if(result==null){
    return true;
    }
    }catch(Exception e){
    connection.commit();
    }
    Thread.sleep(*);
    }
    return false;
    }

    void unlock(){
    connection.commit();
    }
  • version 乐观锁

    乐观锁–每次去拿数据的时候都认为别人不会修改,但是更新的时候会判断有没有其他操作更新了该数据,根据比较版本号(或时间戳)的方式来衡量当前版本是不是最新版本

    1
    2
    select version as oldVersion,... from t_business_table where id=xx
    update t_business_table set version + 1 where version=oldVersion

基于redis实现分布式锁(redis单进程单线程特性)

jedis.set(String key, String value, String nxxx, String expx, int time)

  • 第一个为key,我们使用key来当锁,因为key是唯一的。
  • 第二个为value,我们传的是requestId,很多童鞋可能不明白,有key作为锁不就够了吗,为什么还要用到value?原因就是我们在上面讲到可靠性时,分布式锁要满足第四个条件解铃还须系铃人,通过给value赋值为requestId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。requestId可以使用UUID.randomUUID().toString()方法生成。
  • 第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作;
  • 第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。
  • 第五个为time,与第四个参数相呼应,代表key的过期时间。

总的来说,执行上面的set()方法就只会导致两种结果:

  • 当前没有锁(key不存在),那么就进行加锁操作,并对锁设置个有效期,同时value表示加锁的客户端。
  • 已有锁存在,不做任何操作
  • 加锁

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    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";

    /**
    * 尝试获取分布式锁
    * @param jedis Redis客户端
    * @param lockKey 锁
    * @param requestId 请求标识
    * @param expireTime 超期时间
    * @return 是否获取成功
    */

    public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
    String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);

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

    return false;
    }
  • 解锁

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    private static final Long RELEASE_SUCCESS = 1L;

    /**
    * 释放分布式锁
    * @param jedis Redis客户端
    * @param lockKey 锁
    * @param requestId 请求标识
    * @return 是否释放成功
    */

    public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
    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(lockKey), Collections.singletonList(requestId));

    if (RELEASE_SUCCESS.equals(result)) {
    return true;
    }
    return false;
    }
  • 当前做法

    a)set+expire
    b)setnx+expire

基于zk实现分布式锁

临时有序节点的特性实现,每个客户端对某个功能加锁时,在zookeeper上的与该功能对应的指定节点的目录下,生成一个唯一的临时有序节点,判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个;当释放锁的时候,只需将这个瞬时节点删除即可。

分布式锁设计目标

  • 这把锁要是一把可重入锁(避免死锁,即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁)
  • 这把锁最好是一把阻塞锁(根据业务需求考虑要不要这条)
  • 这把锁有高可用的获取锁和释放锁功能(只要大部分的Redis节点正常运行,客户端就可以加锁和解锁)
  • 这把锁获取锁和释放锁的性能要好
  • 互斥性,在任意时刻,只有一个客户端能持有锁
  • 解铃还须系铃人,加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了
文章目录
  1. 1. 分布式锁应用场景分析
    1. 1.1. 概念
    2. 1.2. 场景
      1. 1.2.1. 账户操作
      2. 1.2.2. 资源池获取任务
      3. 1.2.3. 公有账户给私有账户充值
      4. 1.2.4. 星币扣除
    3. 1.3. 业内解决方案
      1. 1.3.1. 基于Mysql实现分布式锁
      2. 1.3.2. 基于redis实现分布式锁(redis单进程单线程特性)
      3. 1.3.3. 基于zk实现分布式锁
    4. 1.4. 分布式锁设计目标