分布式锁的实现

一、什么是分布式锁?

在多线程环境下,对于共享资源的访问,我们可以通过给线程加锁,来解决线程安全问题,保证数据的一致性。

在分布式架构中,应用程序是集群部署在不同的服务器上,这些应用程序进程之间是隔离的。对于共享资源数据的排他性访问,需要对共享资源加锁,而这个锁需要让所有的进程都访问到。此时,我们需要通过使用分布式锁解决共享资源的访问。使用分布式锁的主要原因是锁(互斥性)的使用范围发生了改变。

分布式锁的核心思想:首先获取锁、然后执行操作、最后释放锁。

分布式锁主要在多进程环境下,对共享资源的访问的场景下应用。比如:秒杀场景下,使用分布式锁防止超卖

二、分布式锁的实现

分布式锁有如下几种实现方式:

  • 基于数据库实现分布式锁。
  • 基于Redis实现分布式锁。
  • 基于zookeeper实现分布式锁

(1)基于数据库实现分布式锁

首先,在数据库中创建一张锁表,表中包含方法名等字段。并在方法名字段上创建唯一约束(唯一索引),通过使用方法名向表中插入数据,成功插入则获取到锁。获取到锁后,执行操作,操作完成后,删除数据释放锁。

实现步骤:
第一步:在数据库新建一张锁表(lock)。

DROP TABLE IF EXISTS `lock`;
CREATE TABLE `lock` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
  `method_name` varchar(64) NOT NULL COMMENT '锁定的方法名',
  `desc` varchar(255) NOT NULL COMMENT '备注信息',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uidx_method_name` (`method_name`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='锁定中的方法';

第二步:获取锁时向表中插入一条数据,由于有唯一约束,只会有一个线程插入成功,插入成功的线程获得锁,可以继续操作,没有插入成功的线程没有获得锁,不能操作。

INSERT INTO lock (method_name, desc) VALUES ('methodName', '测试的methodName');

第三步:解锁时,删除该条数据。

delete from lock where method_name ='methodName';

主要问题:

  1. 可用性差,数据库故障会导致业务系统不可用。
  2. 数据库性能存在瓶颈,不适合高并发场景。
  3. 没有失效机制,删除锁失败容易造成死锁。
  4. 锁的失效时间难以控制,需要通过定时任务去清理失效的key。

(2)基于Redis实现分布式锁

Redis里面提供了一些能够实现互斥特性的命令,比如SETNX (在key不存在的情况下为key设置值,key存在的话就不设置值),那么我们可以基于这些命令来去实现锁。

实现思想如下:

  1. 获取锁时,通过使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间自动释放锁, 锁的value1值为一个随机生成的uuid,用于在释放锁时进行判断。
  2. 获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。
  3. 释放锁的时候,通过uuid判断是不是该锁,若是该锁,则执行delete进行锁释放。
  4. 拿到锁期间,监控redis分布式锁是否需要延期,防止提前释放锁。

利用Redis实现分布式锁主要用到三个命令:

  1. setnx:setnx key value : 设置key及key的值,如果key存在返回设置失败,返回0;key不存在就设置成功,返回1;
  2. expire:expire key timeout: 设置key的过期时间。
  3. delete:delete key: 删除key

代码如下:

package com.lock.redis.jedis;

import com.lock.redis.jedis.util.JedisPoolInstance;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

import java.util.UUID;

public class JedisDistributeLock {
    
    private static final String redisLockPrefix = "redis:lock:";
   
    /**
     * 获取锁
     *
     * @param lockName
     * @param acquireTimeOut 单位是毫秒 获取锁时间
     * @param lockTimeOut    单位是毫秒 超时时间
     * @return
     */
    public String getRedisLock(String lockName, Long acquireTimeOut, Long lockTimeOut) {

        String redisLockKey = redisLockPrefix + lockName;
        String uniqueValue = UUID.randomUUID().toString();

        //通过JedisPool创建一个jedis连接
        JedisPool jedisPool = JedisPoolInstance.getJedisPoolInstance();
        Jedis jedis = jedisPool.getResource();

        try {
            //超时时间 往后延acquireTimeOut秒(比如3秒)
            Long endTime = System.currentTimeMillis() + acquireTimeOut;
            //时间没有超过,有资格获取锁
            while (System.currentTimeMillis() < endTime) {

                //设置key 和 设置key的过期时间 不是原子操作(不在一个步骤中,是分了两步)
                if (jedis.setnx(redisLockKey, uniqueValue) == 1) {
                    //设置key成功,表示拿到锁
                    jedis.pexpire(redisLockKey, lockTimeOut);

                    return uniqueValue;
                } else {
                    if (jedis.ttl(redisLockKey) == -1) {
                        //设置过期时间
                        jedis.pexpire(redisLockKey, lockTimeOut);
                    }
                }

                //立刻马上去循环再获取锁,其实不是很好也没有什么意义,最好是稍等片刻再去重试获取锁
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        } finally {
            if (jedis != null) {
                jedis.close();
            }
        }
        return null;

    }


    /**
     * 释放redis锁
     *
     * @param lockName
     * @param uniqueValue
     */
    public void releaseRedisLock(String lockName, String uniqueValue) {
        //redis锁的key
        String redisLockKey = redisLockPrefix + lockName;

        Jedis jedis = JedisPoolInstance.getJedisPoolInstance().getResource();
        try {
            //自己的锁自己解,不要把别人的锁给解了
            if (jedis.get(redisLockKey).equals(uniqueValue)) {
                jedis.del(redisLockKey);
            }
        } finally {
            if (jedis != null) {
                jedis.close();
            }
        }
    }
}

基于redisson客户端,实现Redis分布式锁方案如下:

redisson已经给我们提供现成的分布式锁,我们直接使用即可。

实现思想如下:

  1. 通过redissonClient 获取锁。
  2. 加锁,执行业务代码,提供了锁自动续期功能。
  3. 解锁,释放锁。

代码如下:

RLock rLock = redissonClient.getLock(lockName);

//加锁,然后下面的业务代码就会按顺序排队执行
rLock.lock(); //它有自动续期的
//执行业务代码
//......
//TODO 解锁,释放锁
if (rLock.isHeldByCurrentThread() && rLock.isLocked()) {
    rLock.unlock();
}

(3)基于zookeeper实现分布式锁

zookeeper实现分布式锁采用其提供的临时有序节点+监听来实现。

实现思想如下:

  1. 创建一个锁的根结点 “/locks”,在构造方法初始化时,创建根节点。
  2. 获取锁时,现在根节点下创建一个临时顺序节点,然后获取根节点下所有子节点,判断当前节点是否为自小节点,如果是最小节点,则获取到锁,执行业务操作。如果不是最小节点,就监听前一个节点的删除事件,当前一个节点删除时,触发我的监听事件,获取到分布式锁。
  3. 释放锁:从zookeeper中删除当前节点。

基于zookeeper实现分布式锁有以下几种方式:

  1. Zookeeper原生客户端实现分布式锁。
  2. ZkClient第三方客户端实现分布式锁。
  3. Curator客户端给我们提供了现成的分布式互斥锁来实现分布式锁,所以我们不必自己开发。

使用Curator客户端实现分布式锁过程如下:

第一步:创建分布式互斥锁。
InterProcessMutex lock = new InterProcessMutex(zookeeperCuratorClient.client, “/storeLock”);

InterProcessMutex lock = new InterProcessMutex(zookeeperCuratorClient.client, "/storeLock");

第二步:获取分布式互斥锁。

if (lock.acquire(10, TimeUnit.SECONDS))

第三步:释放分布式互斥锁。

lock.release();
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值