几种分布式锁的实现方式

简介: 几种分布式锁的实现方式



实现方案 实现思路 优点 缺点
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();
        }
    }
}


目录
相关文章
|
存储 缓存 NoSQL
分布式锁原理与实现(数据库、redis、zookeeper)
分布式锁可以保证在分布式部署的应用集群中,同一个方法在同一时间只能被一台机器上的一个线程执行。
分布式锁原理与实现(数据库、redis、zookeeper)
|
缓存 Java 数据库连接
深入浅出 MyBatis 的一级、二级缓存机制
深入浅出 MyBatis 的一级、二级缓存机制
1286 0
|
SQL 关系型数据库 数据库
学习分布式事务Seata看这一篇就够了,建议收藏
学习分布式事务Seata看这一篇就够了,建议收藏
26212 2
|
9月前
|
存储 关系型数据库 调度
微服务原理篇(XXLJOB-幂等-MySQL)
本课程深入讲解微服务核心组件XXL-JOB任务调度原理,涵盖其架构、分布式任务处理、幂等性设计及MySQL存储引擎、索引机制、SQL优化与分库分表策略,全面提升系统性能与可靠性。
|
10月前
|
缓存 Shell API
解决mac电脑brew update很慢的问题
Homebrew 大部分都是 API 优先模式,切换国内源需配置 API 镜像而非仅修改 git 仓库。核心是设置 `HOMEBREW_API_DOMAIN` 指向国内镜像(如清华源),并更新 brew 主仓库地址,最后执行 `brew update` 生效。旧方法已不适用新版本。
1821 5
|
canal 缓存 NoSQL
Redis缓存与数据库如何保证一致性?同步删除+延时双删+异步监听+多重保障方案
根据对一致性的要求程度,提出多种解决方案:同步删除、同步删除+可靠消息、延时双删、异步监听+可靠消息、多重保障方案
Redis缓存与数据库如何保证一致性?同步删除+延时双删+异步监听+多重保障方案
|
消息中间件 存储 Kafka
RocketMQ 工作原理图解,看这篇就够了!
本文详细解析了 RocketMQ 的核心架构、消息领域模型、关键特性和应用场景,帮助深入理解消息中间件的工作原理。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
RocketMQ 工作原理图解,看这篇就够了!
|
XML Java 数据格式
最近很火的SOFARPC是什么?带你快速入门SOFARPC
最近很火的SOFARPC是什么?带你快速入门SOFARPC
最近很火的SOFARPC是什么?带你快速入门SOFARPC

热门文章

最新文章