Redis分布式锁

背景

在集群环境下,由于我们的服务是部署在多个机器上的,就会出现多个进程同时操作一份数据的情况,比如说,创建订单的服务是部署在A、B两台机器上,小明和小王买苹果的订单请求分别打到A、B两台机器,收到请求后,A、B两台机器都会去查询数据库,稍后又会对数据做一些修改,很显然,这种情况是需要锁的,不然会出现超卖事故。

加锁可不可以加本地锁呢?像常用的lock和synchronized,当然是不可以的。目前的场景是来自不同机器的请求要去修改数据,假如机器A加了一个本地锁,那机器B它也不知道你加了锁呀,因为你们是在不同的JVM里面。因此这个时候就需要有分布式锁了。
分布式锁的实现方式有很多种,这里记录下redis是如何实现分布式锁的。

redis实现分布式锁

set lock true ex 5 nx
do something...
del lock

指令表示的是,如果不存在key = lock,我们就添加一个键值对(key, true),过期时间为5s,指令执行完后会返回1,客户端就知道自己加锁成功,可以继续操作,如果加锁失败,则会返回0,客户端该干啥干啥。注意这里的key是任意的,但命名通常与业务有关。

这里存在一个问题,就是超时问题,假如机器上的进程A加锁成功,然后开始do something,但是这个something有点长,超过了5s,锁就会过期,这个时候,另一台机器上的进程B会拿到这把锁,也开始do something,很大几率上,他们两个会同时操作临界区的资源;此外,更严重的是,当进程A执行完后,由于它并不知道它的锁已经过期被自动释放了,它再一次释放锁,然而这个锁是进程B加的,而此时进程B的逻辑可能还没有执行完,但因为锁被释放了,其它的进程又可以加锁成功,形成了一个恶性循环。

利用jedis实现的分布式锁

public class JedisDistributeLocker implements DistributeLocker {

    public static int LOCKEXPIRETIME = 3000;

    JedisPool jedisPool = new JedisPool("127.0.0.1", 6379);;

    @Override
    public boolean tryLock(String key, String tag) {
        return tryGetLock(key, tag, LOCKEXPIRETIME);
    }

    private boolean tryGetLock(String key, String tag, int lockexpiretime) {
        Jedis jedisCli = jedisPool.getResource();
        long now = System.currentTimeMillis();

        try {
            String result = jedisCli.set(key, tag, "NX", "EX", lockexpiretime);
            if ("OK".equalsIgnoreCase(result)) {
                System.out.println(Thread.currentThread().getName() +
                        ": 加锁成功, key = " + key + ", value = " + tag + ", 耗时: " + (System.currentTimeMillis() - now));
                return true;
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (jedisCli == null) {
                jedisCli.close();
            }
        }

        return false;
    }

    @Override
    public void lock(String key, String tag, long timeoutMills) {
        long now = System.currentTimeMillis();
        try {
            while (System.currentTimeMillis() - now <= timeoutMills) {
                if (tryLock(key, tag)) {
                    return;
                }
            }
            throw new Exception("超时: " + (System.currentTimeMillis() - now));
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }

    @Override
    public void unlock(String key, String tag) {
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Jedis jedisCli = jedisPool.getResource();

        try {
            Object obj = jedisCli.eval(script, Collections.singletonList(key), Collections.singletonList(tag));
            System.out.println(obj);
            System.out.println(Thread.currentThread().getName() +
                    ": 解锁成功, key = " + key + ", value = " + tag);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (jedisCli == null) {
                jedisCli.close();
            }
        }
    }
}

lua脚本的作用是在del分布式锁的时候,判断是不是我之前加的锁,避免将别的服务加的锁del掉(可能服务的执行逻辑过长,导致锁过期失效,让别的服务有可乘之机)。

Redisson分布式锁

相比如redis的setnx命令,Redisson这里又进一步的对分布式锁做了封装,可以很好的解决超时导致锁失效的问题。

引入redisson依赖

<dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.16.0</version>
</dependency>

测试demo

public class RedissonDemo {

    public static void main(String[] args) throws InterruptedException {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        RedissonClient redisson = Redisson.create(config);

        RLock lock = redisson.getLock("anyKey");

        long tId = Thread.currentThread().getId();

        lock.lock();

        Thread.sleep(10000L);

        lock.unlock();

        redisson.shutdown();
    }
}

从lock断点进入到可重入锁的实现,,在类RedissonLock的tryLockInnerAsync方法入参中可以找到它的加锁脚本。

KEYS[1]:就是key 
ARGV[1]:该锁的过期时间
ARGV[2]:key对应的哈希key,与线程名有关,是线程独有的

脚本的意思是:
首先判断key存不存在,如果不存在,设置一个key,哈希值为1,然后设置过期时间;
如果key存在,并且哈希key也一样,说明是我之前加的锁,将哈希值加一,设置过期时间(这是一个可重入锁);
如果key存在,并且是别线程设置的,则返回该锁当前的失效时间。

"if (redis.call('exists', KEYS[1]) == 0) then 
	redis.call('hincrby', KEYS[1], ARGV[2], 1); 
	redis.call('pexpire', KEYS[1], ARGV[1]); 
	return nil; 
end; 
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then 
	redis.call('hincrby', KEYS[1], ARGV[2], 1); 
	redis.call('pexpire', KEYS[1], ARGV[1]); 
	return nil; 
end; 
return redis.call('pttl', KEYS[1]);"

如果仅仅是这样,那和jedis也没什么区别,事实上,redisson是可以解决超时问题的。

首先,超时问题可能带来的后果是别的进程获取到了锁,两个进程同时操作数据。如果一开始你指定了超时时间,那么在系统运行正常的情况下,你就应该在超时时间内完成,不然你没有必要设置时间;如果设置了时间,并且可以在超时时间内完成,但系统宕机了,没关系,即使别的进程拿到了锁,但还是只有一个进程在操作。

redisson是在未指定超时时间时,会给锁自动续期,防止锁失效。在未指定锁过期时间时,默认是30s,当加锁成功后,每过10s会执行以下脚本,也就是给锁续期。

if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then 
	redis.call('pexpire', KEYS[1], ARGV[1]); 
	return 1; 
end; 
return 0;
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值