几种分布式锁的实现方式

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
注册配置 MSE Nacos/ZooKeeper,118元/月
简介: 几种分布式锁的实现方式



实现方案 实现思路 优点 缺点
mysql实现方案 利用数据库自身提供的锁机制实现,要求数据库支持行级锁 实现简单,稳定 性能好,无法适应高并发场景;容易出现死锁;无法优雅实现阻塞式锁
redis的实现方案 使用Setnx和lua脚本机制实现,保证对缓存序列的原子性 性能好 实现较复杂;有出现死锁的可能性;无法优雅的实现阻塞式锁
zookeeper实现方案 基于zk的节点特性以及watch机制实现 性能好,稳定可靠性高,能较好的实现阻塞式锁 实现相对复杂


在分布式系统中,各系统同步访问共同的资源是很常见的。因此我们常常需要协调他们的动作。 如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,在这种情况下,便需要使用到分布式锁。

一个好的分布式锁常常需要以下特性:

  • 可重入
  • 同一时间点,只有一个线程持有锁
  • 容错性, 当锁节点宕机时, 能及时释放锁
  • 高性能
  • 无单点问题

一. 基于数据库的分布式锁

基于数据库的分布式锁, 常用的一种方式是使用表的唯一约束特性。当往数据库中成功插入一条数据时, 代表只获取到锁。将这条数据从数据库中删除,则释放送。

因此需要创建一张锁表

CREATE TABLE `methodLock` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `method_name` varchar(64) NOT NULL DEFAULT '' COMMENT '锁定的方法名',
  `cust_id` varchar(1024) NOT NULL DEFAULT '客户端唯一编码',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '保存数据时间,自动生成',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uidx_method_name` (`method_name `) USING BTREE
)
 ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='锁定中的方法';
复制代码

添加锁

insert into methodLock(method_name,cust_id) values (‘method_name’,‘cust_id’)
复制代码

这里cust_id 可以是机器的mac地址+线程编号, 确保一个线程只有唯一的一个编号。

通过这个编号, 可以有效的判断是否为锁的创建者,从而进行锁的释放以及重入锁判断

释放锁

delete from methodLock where method_name ='method_name' and cust_id = 'cust_id'
复制代码

重入锁判断

select 1 from methodLock where method_name ='method_name' and cust_id = 'cust_id'
复制代码

加锁以及释放锁的代码示例

/**
* 获取锁
*/
public boolean lock(String methodName){
    boolean success = false;
    //获取客户唯一识别码,例如:mac+线程信息
    String custId = getCustId();
    try{
        //添加锁
       success = insertLock(methodName, custId);
    } catch(Exception e) {
        //如添加失败
    }
    return success;
}
/**
* 释放锁
*/
public boolean unlock(String methodName) {
    boolean success = false;
    //获取客户唯一识别码,例如:mac+线程信息
    String custId = getCustId();
    try{
        //添加锁
       success = deleteLock(methodName, custId);
    } catch(Exception e) {
        //如添加失败
    }
    return success;
}
复制代码

完整流程

public void test() {
    String methodName = "methodName";
    //判断是否重入锁
    if (!checkReentrantLock(methodName)) {
        //非重入锁
        while (!lock(methodName)) {
            //获取锁失败, 则阻塞至获取锁
            try{
                Thread.sleep(100)
            } catch(Exception e) {
            }
        }
    }
    //TODO 业务处理
    
    //释放锁
    unlock(methodName);
}
复制代码

以上代码还存在一些问题:

  • 没有失效时间。 解决方案:设置一个定时处理, 定期清理过期锁
  • 单点问题。 解决方案: 弄几个备份数据库,数据库之前双向同步,一旦挂掉快速切换到备库上

二. 基于redis的分布式锁

使用redis 的set(String key, String value, String nxxx, String expx, int time)命令

  • 第一个为key,我们使用key来当锁,因为key是唯一的。
  • 第二个为value,我们传的是custId,这里cust_id 可以是机器的mac地址+线程编号, 确保一个线程只有唯一的一个编号。通过这个编号, 可以有效的判断是否为锁的创建者,从而进行锁的释放以及重入锁判断
  • 第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作
  • 第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。
  • 第五个为time,与第四个参数相呼应,代表key的过期时间。

代码示例

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;
// Redis客户端
private Jedis jedis;
/**
 * 尝试获取分布式锁
 * @param lockKey 锁
 * @param expireTime 超期时间
 * @return 是否获取成功
 */
public boolean lock(String lockKey, int expireTime) {
    //获取客户唯一识别码,例如:mac+线程信息
    String custId = getCustId();
    String result = jedis.set(lockKey, custId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
    if (LOCK_SUCCESS.equals(result)) {
        return true;
    }
    
    return false;
}
/**
 * 释放分布式锁
 * @param lockKey 锁
 * @param requestId 请求标识
 * @return 是否释放成功
 */
public boolean unlock(String lockKey,) {
    //获取客户唯一识别码,例如:mac+线程信息
    String custId = getCustId();
    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(custId));
    if (RELEASE_SUCCESS.equals(result)) {
        return true;
    }
    return false;
}
/**
 * 获取锁信息
 * @param lockKey 锁
 * @return 是否重入锁
 */
public boolean checkReentrantLock(String lockKey){
    //获取客户唯一识别码,例如:mac+线程信息
    String custId = getCustId();
    
    //获取当前锁的客户唯一表示码
    String currentCustId = redis.get(lockKey);
    if (custId.equals(currentCustId)) {
        return true;
    }
    return false;
}
复制代码

完整流程

public void test() {
    String lockKey = "lockKey";
    //判断是否重入锁
    if (!checkReentrantLock(lockKey)) {
        //非重入锁
        while (!lock(lockKey)) {
            //获取锁失败, 则阻塞至获取锁
            try{
                Thread.sleep(100)
            } catch(Exception e) {
            }
        }
    }
    //TODO 业务处理
    
    //释放锁
    unlock(lockKey);
}
复制代码

三. 基于memcached的分布式锁

memcached的实现方式和redis类似, 使用的是命令add(key, value, expireDate),注:仅当缓存中不存在键时,才会添加成功

  • 第一个为key,我们使用key来当锁,因为key是唯一的。
  • 第二个为value,我们传的是custId,这里cust_id
  • 第三个为expireDate, 设置一个过期时间,比如: new Date(1000*10),则表示十秒之后从Memcached内存缓存中删除)。

代码示例

// Redis客户端
private MemCachedClient memCachedClient;
/**
 * 尝试获取分布式锁
 * @param lockKey 锁
 * @param expireTime 超期时间
 * @return 是否获取成功
 */
public boolean lock(String lockKey, Date expireDate) {
    //获取客户唯一识别码,例如:mac+线程信息
    String custId = getCustId();
    Boolean result = false;
    try {
        result = memCachedClient.add(lockKey, custId,expireDate);
    } catch(Excetion e) {
    }
    return result;
}
/**
 * 释放分布式锁
 * @param lockKey 锁
 * @param requestId 请求标识
 * @return 是否释放成功
 */
public boolean unlock(String lockKey,) {
    //获取客户唯一识别码,例如:mac+线程信息
    //获取客户唯一识别码,例如:mac+线程信息
    String custId = getCustId();
    Boolean result = false;
    try {
        String currentCustId = memCachedClient.get(lockKey);
        if (custId.equals(currentCustId)) {
            result = memCachedClient.delete(lockKey, custId,expireDate);
        }
    } catch(Excetion e) {
    }
    return result;
}
/**
 * 获取锁信息
 * @param lockKey 锁
 * @return 是否重入锁
 */
public boolean checkReentrantLock(String lockKey){
    //获取客户唯一识别码,例如:mac+线程信息
    String custId = getCustId();
    //获取当前锁的客户唯一表示码
    try {
         String currentCustId = memCachedClient.get(lockKey);
        if (custId.equals(currentCustId)) {
            return true;
        }
    } catch(Excetion e) {
    }
   
    return false;
}
复制代码

完整流程

public void test() {
    String lockKey = "lockKey";
    //判断是否重入锁
    if (!checkReentrantLock(lockKey)) {
        //非重入锁
        while (!lock(lockKey)) {
            //获取锁失败, 则阻塞至获取锁
            try{
                Thread.sleep(100)
            } catch(Exception e) {
            }
        }
    }
    //TODO 业务处理
    
    //释放锁
    unlock(lockKey);
}
复制代码

四. 基于zookeeper的分布式锁

基于zookeeper临时有序节点可以实现的分布式锁。 大致思想即为:每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。

可以直接使用zookeeper第三方库Curator客户端,这个客户端中封装了一个可重入的锁服务。

完整流程

public void test() {
    //Curator提供的InterProcessMutex是分布式锁的实现。通过acquire获得锁,并提供超时机制,release方法用于释放锁。
    InterProcessMutex lock = new InterProcessMutex(client, ZK_LOCK_PATH);
    try {
        //获取锁
        if (lock.acquire(10 * 1000, TimeUnit.SECONDS)) {
            //TODO 业务处理
        }
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        try {
            //释放锁
            lock.release();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}


相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore     ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库 ECS 实例和一台目标数据库 RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
23天前
|
NoSQL 数据库 Redis
分布式锁的实现方案有哪些
分布式锁用于协调跨多个节点的任务执行。基于数据库的分布式锁利用唯一性约束或悲观锁确保锁的唯一性;Redis 实现则依赖 SETNX 指令或 redisson 客户端,通过原子操作保证互斥性;ZooKeeper 通过临时顺序节点与 Watch 机制,实现锁的竞争、释放及获取。
35 4
|
4月前
|
NoSQL 关系型数据库 MySQL
分布式锁设计问题之分布式锁内部实现的如何解决
分布式锁设计问题之分布式锁内部实现的如何解决
|
7月前
|
缓存 NoSQL 数据库
分布式锁三种实现方式及对比
分布式锁三种实现方式及对比
180 0
|
7月前
|
存储 缓存 NoSQL
分布式锁的常见实现方式有哪些
分布式锁的常见实现方式有哪些
|
7月前
|
存储 NoSQL 关系型数据库
理解分布式锁的实现过程
理解分布式锁的实现过程
60 0
|
存储 NoSQL 关系型数据库
分布式锁的实现方式
分布式锁的实现方式
76 0
|
NoSQL 安全 关系型数据库
浅谈分布式锁实现原理
浅谈分布式锁实现原理
73 0
|
移动开发 NoSQL Redis
从源码分析Redis分布式锁的原子性保证(一)
从源码分析Redis分布式锁的原子性保证
230 0
|
NoSQL Redis
从源码分析Redis分布式锁的原子性保证(二)
从源码分析Redis分布式锁的原子性保证
138 0
|
NoSQL 算法 中间件
分布式锁的 3 种实现方案
分布式锁的 3 种实现方案
216 0