当前位置:   article > 正文

【Redis】Redis 如何实现分布式锁_redis如何实现分布式锁

redis如何实现分布式锁

1. 什么是分布式锁

分布式锁其实就是,控制分布式系统不同进程共同访问共享资源的一种锁的实现。如果不同的系统或同一个系统的不同主机之间共享了某个临界资源,往往需要互斥来防止彼此干扰,以保证一致性

1.1 分布式锁的特点

在这里插入图片描述

  1. 互斥性:任意时刻,只有一个客户端能持有锁;

  2. 可重入性:一个线程获取锁之后,可以再次对其请求加锁;

  3. 锁超时释放:持有锁超时释放,防止不必要的资源浪费,也可以防止死锁;

  4. 高效、高可用:加锁和解锁需要高效,同时也需要保证高可用防止分布式锁失效;

  5. 安全性:锁只能被持有的客户端删除,不能被其他客户端删除。

1.2 分布式锁的场景

  • 使用分布式锁的场景一般需要满足以下场景:

    • 系统是一个分布式系统,Java 的锁已经锁不住了;
    • 操作共享资源,比如库里唯一的用户数据;
    • 同步访问,即多个进程同时操作共享资源。
  • 分布式锁的业务场景

    • 扣减库存
    • 抢红包

1.3 分布式锁的实现方式

  1. 数据库乐观锁;

  2. 基于 ZooKeeper 的分布式锁;

  3. 基于 Redis 的分布式锁。

这里主要介绍使用 Redis 实现分布式锁的方案。

2. Redis 实现分布式锁

2.1 setnx + expire

SETNXSET IF NOT EXISTS 的简写。命令格式是 SETNX key value,如果 lockKey 不存在,则 SETNX 成功返回 1,如果这个 lockKey 已经存在了,则返回 0。

伪代码:

// 1. 加锁
if(jedis.setnx(lockKey, lockValue) == 1{
    // 2. 设置过期时间
    jedis.expire(lockKey, expireTime);
    try {
        // 3. 业务处理
        do something;
    } catch (Exception e) {
        log.error("处理失败,", e);
    } finally {
        // 4. 释放锁
        jedis.del(lockKey);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

在这个方案中 setnxexpire 「不是原子操作」,如果执行完第一步 jedis.setnx() 加锁后异常了,第二步 jedis.expire() 未执行,相当于这个锁没有过期时间,「有产生死锁的可能」。对这个问题如何改进?

2.2 set ex px nx

基于 Redis 的 SET 扩展命令(SET key value[EX seconds][PX milliseconds][NX|XX]),保证 SETNX + EXPIRE 两条指令的原子性。

  • EX second :设置键的过期时间为 second 秒;

  • PX millisecond :设置键的过期时间为 millisecond 毫秒;

  • NX :表示 key 不存在的时候,才能 set 成功,也即保证只有第一个客户端请求才能获得锁,而其他客户端请求只能等其释放锁,才能获取;

  • XX :只在键已经存在时,才对键进行设置操作。

伪代码:

// 1. 加锁并设置过期时间
if(jedis.set(lockKey, lockValue, "NX", "EX", 100s) == 1){
    try {
        // 2. 业务处理
        do something;
    } catch (Exception e) {
        log.error("处理失败,", e);
    } finally {
        // 3. 释放锁
        jedis.del(lockKey);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

在这个方案中存在两个问题:

  1. 锁过期释放了,业务还没执行完。假设线程a获取锁成功,一直在执行临界区的代码。但是100s过去后,它还没执行完。但是,这时候锁已经过期了,此时线程b又请求过来。显然线程b就可以获得锁成功,也开始执行临界区的代码。那么问题就来了,临界区的业务代码都不是严格串行执行的了。

  2. 锁被别的线程误删。假设线程a执行完后,去释放锁。但是它不知道当前的锁可能是线程b持有的(线程a去释放锁时,有可能过期时间已经到了,此时线程b进来占有了锁)。那线程a就把线程b的锁释放掉了,但是线程b临界区业务代码可能都还没执行完。

2.3 set ex px nx + 校验唯一随机值,再删除

既然锁可能被别的线程误删,那我们给 value 值设置一个标记当前线程唯一的随机数,在删除的时候,校验一下,就可以了。

伪代码:

 // 1. 加锁并设置过期时间
 if(jedis.set(lockKey, uni_lockValue, "NX", "EX", 100s) == 1){
     try {
         // 2. 业务处理
         do something;
     } catch (Exception e) {
         log.error("处理失败,", e);
     } finally {
         // 3. 判断是不是当前线程加的锁
         if (uni_lockValue.equals(jedis.get(lockKey))) {
             // 4. 释放锁
             jedis.del(lockKey);
         }
     }
 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

这里的 3. 是非原子性的,我们使用 lua 脚本来优化一下

if redis.call('get',KEYS[1]) == ARGV[1] then 
   return redis.call('del',KEYS[1]) 
else
   return 0
end;
  • 1
  • 2
  • 3
  • 4
  • 5

这个方案还是会存在 「锁过期释放,业务没执行完」 的问题,有些小伙伴认为,稍微把锁过期时间设置长一些就可以了。其实我们设想一下,是否可以给获得锁的线程,开启一个定时守护线程,每隔一段时间检查锁是否还存在,存在则对锁的过期时间延长,防止锁过期提前释放。

2.4 Redisson 实现分布式锁

在这里插入图片描述
只要线程一加锁成功,就会启动一个 watch dog 看门狗,它是一个后台线程,会每隔 10s 检查一下,如果 线程1 还持有锁,那么就会不断的延长锁 key 的生存时间。Redisson 完美解决了「锁过期释放,业务没执行完」问题。

Redisson lock 和 tryLock 原理解析

package com.pointer.mall.common.util;

import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;

/**
 * @author gaoyang
 * @date 2023-02-23 20:24
 */
@Slf4j
@Component
public class RedissonUtil {

    @Resource
    private RedissonClient redissonClient;

    /**
     * 加锁
     *
     * @param lockKey
     */
    public void lock(String lockKey) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.lock();
    }

    /**
     * 带过期时间的锁
     *
     * @param lockKey   key
     * @param leaseTime 上锁后自动释放锁时间
     */
    public void lock(String lockKey, long leaseTime) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.lock(leaseTime, TimeUnit.SECONDS);
    }

    /**
     * 带超时时间的锁
     *
     * @param lockKey   key
     * @param leaseTime 上锁后自动释放锁时间
     * @param unit      时间单位
     */
    public void lock(String lockKey, long leaseTime, TimeUnit unit) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.lock(leaseTime, unit);
    }

    /**
     * 尝试获取锁
     *
     * @param lockKey key
     * @return
     */
    public boolean tryLock(String lockKey) {
        RLock lock = redissonClient.getLock(lockKey);
        return lock.tryLock();
    }

    /**
     * 尝试获取锁
     *
     * @param lockKey   key
     * @param waitTime  最多等待时间
     * @param leaseTime 上锁后自动释放锁时间
     * @return boolean
     */
    public boolean tryLock(String lockKey, long waitTime, long leaseTime) {
        RLock lock = redissonClient.getLock(lockKey);
        try {
            return lock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            log.error("RedissonUtils - tryLock异常", e);
        }

        return false;
    }

    /**
     * 尝试获取锁
     *
     * @param lockKey   key
     * @param waitTime  最多等待时间
     * @param leaseTime 上锁后自动释放锁时间
     * @param unit      时间单位
     * @return boolean
     */
    public boolean tryLock(String lockKey, long waitTime, long leaseTime, TimeUnit unit) {
        RLock lock = redissonClient.getLock(lockKey);
        try {
            return lock.tryLock(waitTime, leaseTime, unit);
        } catch (InterruptedException e) {
            log.error("RedissonUtils - tryLock异常", e);
        }

        return false;
    }

    /**
     * 释放锁
     *
     * @param lockKey key
     */
    public void unlock(String lockKey) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.unlock();
    }

    /**
     * 是否存在锁
     *
     * @param lockKey key
     * @return
     */
    public boolean isLocked(String lockKey) {
        RLock lock = redissonClient.getLock(lockKey);
        return lock.isLocked();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/菜鸟追梦旅行/article/detail/696769
推荐阅读
相关标签
  

闽ICP备14008679号