【Redis】简单实现分布式锁

目录标题

  • 思路
    • [🔺 如何加锁、解锁](#🔺 如何加锁、解锁)
    • [🔺 如果获取锁当前失败了,如何进行重试](#🔺 如果获取锁当前失败了,如何进行重试)
    • [🔺 加锁、解锁:如何考虑锁的重入问题](#🔺 加锁、解锁:如何考虑锁的重入问题)
    • [🔺 加锁、解锁的唯一性:防止误删除、独占排他性](#🔺 加锁、解锁的唯一性:防止误删除、独占排他性)
    • [🔺 服务器宕机:Redis 宕机](#🔺 服务器宕机:Redis 宕机)
    • [🔺 锁的自动续期:程序操作时间比加锁的时候长](#🔺 锁的自动续期:程序操作时间比加锁的时候长)
    • [🔺 保证高可用:在集群模式下,会导致锁机制失效](#🔺 保证高可用:在集群模式下,会导致锁机制失效)
  • 优化
  • 代码实现
  • 总结

思路

🔺 如何加锁、解锁

使用基本的命令 SETNX 以及 DEL 指令

🔺 如果获取锁当前失败了,如何进行重试

自旋重试

🔺 加锁、解锁:如何考虑锁的重入问题

利用 HINCRBY 的命令 +1 / -1

🔺 加锁、解锁的唯一性:防止误删除、独占排他性

加锁、解锁的时候,利用给每个锁加个唯一的标识来实现

🔺 服务器宕机:Redis 宕机

获取锁成功以后宕机,也就是在Redis里面存在了一条 k-v 键值对的锁了

加锁的时候,添加上过期时间

🔺 锁的自动续期:程序操作时间比加锁的时候长

先 HEXIST 判断有没有锁存在,如果存在,则使用定时任务进行自动续期

🔺 保证高可用:在集群模式下,会导致锁机制失效

红锁算法


优化

在上面可以得出,我们会有一些逻辑性的判断,再加上 Redis 本身是不支持事务的,所以我们就需要有一个Lua 脚本来辅助我们进行实现。

java 复制代码
加锁:setnx key value:1. 独占排它锁

解锁:del key

重试:递归 or 循环 

过期时间:expire:
2. 避免发生死锁(Redis客户端从Redis服务中获取到锁以后立刻宕机 / 锁重入) 

3. 原子性:加锁与过期时间、判断锁与释放锁

4. 防止误删除 (加锁与解锁必须的原子性,即谁加锁就是谁解锁 + 先判断在删除)

5. 自动续期 (保证程序的运行时间与锁的存活时间保持一致)

6. 在集群模式下,会导致锁机制失效
	1. 客户端10010,从主中获取到锁
	2. 由于主还没来得及同步从数据,主宕机,从升级为主
	3. 客户端10086,从新主中获取到锁

==========================(进一步优化)==================================
解决1,2,(宕机)3 问题:
	加锁:set key value [ex seconds] [ px milliseconds] [nx | xx] 
	设 k - v,[过期时间 seconds 秒] [过期时间 milliseconds 毫秒] [只在键不存在 | 只在键存在, 才对键进行设置操作]
------------------------------------------------
解决2(锁重入):hash + lua脚本 

hset lockName ownerLockName ownNum 

加锁:		
	1. 判断锁是否存在且无人拥有?  EXISTS key [key ...]
		是:获取锁 HSET key field value
			判断是不是自己的锁 HEXISTS key field
				是:重入 HINCRBY key field increment
				否:重试 递归或者循环
		否:重试 递归或者循环		
	# lua脚本1.0:基本实现逻辑
		if redis.call('EXISTS', 'lock') == 0
		then 
			// redis.call('HSET', lock , uuid, 1) 等价于
			redis.call('HINCRBY', 'lock', uuid ,1)
			redis.call('expire', lock, 30)
			return 1
		elseif redis.call('HEXISTS', 'lock', uuid) == 1
		then 
			redis.call('HINCRBY', 'lock', uuid ,1)
			redis.call('expire', lock, 30)
			return 1
		else 
			return 0
		end
	# lua脚本2.0:进一步优化
		if redis.call('EXISTS', 'lock') == 0 or redis.call('HEXISTS', 'lock', uuid) == 1
		then
			redis.call('HINCRBY', 'lock', uuid ,1)
			redis.call('expire', lock, 30)
			return 1
		else
			return 0
		end
	# lua脚本3.0:将形参放到脚本之中
		if redis.call('EXISTS', KEYS[1]) == 0 or redis.call('HEXISTS', KEYS[1], ARGV[1]) == 1
		then
			redis.call('HINCRBY', KEYS[1], ARGV[1] ,1)
			redis.call('expire', KEYS[1], ARGV[2])
			return 1
		else
			return 0
		end
	key: lock
    arg: uuid 30
    # lua脚本4.0:转化为一行
    		if redis.call('EXISTS', KEYS[1]) == 0 or redis.call('HEXISTS',KEYS[1], ARGV[1]) == 1 then redis.call('HINCRBY', KEYS[1], ARGV[1] ,1) redis.call('expire', KEYS[1], ARGV[2]) return 1 else return 0 end
    		
解锁:
	1. 判断锁是否存在,且是否为自己的锁,不存在则返回 恶意释放锁nil;
	2. 如果自己的锁存在,则 减1,判断减1 后的值是否为0,为0则释放锁,返回 全部解锁成功1;
	3. 不为0,则代表已经对锁释放了一次,返回 释放锁一次执行成功0
	
	# lua脚本1.0:基本实现
	if redis.call('HEXISTS', KEYS[1], ARGV[1]) == 0
	then 
		return nil
	elseif redis.call('HINCRBY', KEYS[1], ARGV[1] , -1) == 0
    then 
    	return redis.call('del', KEYS[1])
    else
    	return 0
    end 
    key: lock
    arg: uuid 
    # lua脚本2.0:基本实现
    if redis.call('HEXISTS', KEYS[1], ARGV[1]) == 0 then return nil elseif redis.call('HINCRBY', KEYS[1], ARGV[1] , -1) == 0 then return redis.call('del', KEYS[1]) else return 0 end 	
-------------------------------------------
	
解决4问题:防止误删除 :给每个锁加个唯一的标识 + 解锁用lua脚本
	127.0.0.1:6379> eval "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end" 1 lock 2
(integer) 0
-------------------------------------------
解决5问题:定时任务(Timer定时器) + lua 脚本
	判断自己的锁是否存在(hexist),如果存在则重置过期时间
	
	if redis.call('HEXISTS', KEYS[1], ARGV[1]) == 1
	then 
		return redis.call('expire',KEYS[1],ARGV[2])
	else 
    	return 0
    end	
	
	key: lock
	arg: uuid 30
	
	if redis.call('HEXISTS', KEYS[1], ARGV[1]) == 1 then return redis.call('expire',KEYS[1],ARGV[2]) else return 0 end	
	
6. 解决6 (在集群模式下,会导致锁机制失效	)
	红锁算法:多个Redis,相互独立,没有主从关系
		1.  应用程序获取到系统当前时间T1
		2.  应用程序使用相同的 k-v 一次从多个Redis实例中获取到锁,其中获取锁的尝试时间需要设置,以便于尽快访问下一个结点
		3.  计算 获取锁的消耗时间CT1 = 应用用程序获取到系统当前时间T2 - T1
			成功获取到锁:获取锁的消耗时间CT1 < 应用程序总的锁定时间 ,且 半数以上的结点获取锁成功
		4.  计算剩余锁定时间 = 应用程序总的锁定时间 - 获取锁的消耗时间CT1
		5.  如果获取锁失败了,对所有的Redis节点释放锁

代码实现

java 复制代码
public class RedisLockUtil implements Lock {

    private StringRedisTemplate redisTemplate;

    private String lockName;

    private String uuid;

    // 秒为单位
    private long expire = 30;

    public RedisLockUtil(StringRedisTemplate redisTemplate, String lockName, String uuid) {
        this.redisTemplate = redisTemplate;
        this.lockName = lockName;
        this.uuid = uuid + ":" + Thread.currentThread().getId();
    }

    @Override
    public void lock() {
        this.tryLock();
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {

    }

    @Override
    public boolean tryLock() {
        try {
            return this.tryLock(-1L, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return false;
    }

    /**
     * 加锁
     *
     * @param time
     * @param unit
     * @return boolean
     */
    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        if (time != -1) {
            this.expire = unit.toSeconds(time);
        }
        String script = "if redis.call('EXISTS', KEYS[1]) == 0 or redis.call('HEXISTS',KEYS[1], ARGV[1]) == 1 " +
                "then " +
                "redis.call('HINCRBY', KEYS[1], ARGV[1] ,1) " +
                "redis.call('expire', KEYS[1], ARGV[2]) " +
                "return 1 " +
                "else " +
                "return 0 " +
                "end";

        // 如果并没有抢占到锁,则稍等一会,在去抢锁
        while (!this.redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuid, String.valueOf(expire))) {
            try {
                TimeUnit.MILLISECONDS.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 加锁成功,只有等到锁删除成功,定时任务才不会执行,否则开启定时任务自动续期
        this.reNewExpire();
        return true;
    }

    /**
     * 解锁
     */
    @Override
    public void unlock() {
        String script = "if redis.call('HEXISTS', KEYS[1], ARGV[1]) == 0 " +
                "then " +
                "return nil " +
                "elseif redis.call('HINCRBY', KEYS[1], ARGV[1] , -1) == 0 " +
                "then " +
                "return redis.call('del', KEYS[1]) " +
                "else " +
                "return 0 " +
                "end ";
        Long flag = this.redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName), uuid);
        if (flag == null) {
            throw new IllegalMonitorStateException();
        }
    }

    @Override
    public Condition newCondition() {
        return null;
    }

    /**
     * 锁的自动续期
     */
    private void reNewExpire() {
        String script = "if redis.call('HEXISTS', KEYS[1], ARGV[1]) == 1 " +
                "then " +
                "return redis.call('expire',KEYS[1],ARGV[2]) " +
                "else " +
                "return 0 " +
                "end";

        // 执行一次定时任务,定时任务的延期时间为 过期时间的1/3
        new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                // 如果续期成功,则准备是否再次续期
                if (redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuid, String.valueOf(expire))) {
                    reNewExpire();
                }
            }
            // 由于delay 是以毫秒为单位的,所以得乘以 1000  转化为 秒
        }, this.expire * 1000 / 3);
    }
}
java 复制代码
@Component
public class LockClient {

    @Autowired
    private StringRedisTemplate redisTemplate;

    private String uuid;

    public LockClient(){
        this.uuid = UUID.randomUUID().toString();
    }

    public RedisLockUtil getLockClient(String lockName) {
        return new RedisLockUtil(redisTemplate, lockName, uuid);
    }
}
java 复制代码
@Service
public class DbStockService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private LockClient lockClient;

    /**
     * Redis 乐观锁
     */
    public void decrementStock() {
        RedisLockUtil lockClient = this.lockClient.getLockClient("lock");
        lockClient.lock();
        try {
            String stock = redisTemplate.opsForValue().get("stock");
            Integer integer = Integer.valueOf(stock);
            if (integer > 0) {
                redisTemplate.opsForValue().set("stock", String.valueOf(integer - 1));
            }
            // 测试锁的可重入性
            // test();

            // 测试锁的自动续期
            // try { TimeUnit.SECONDS.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); }
        } finally {
            lockClient.unlock();
        }
    }
    
    /**
     * 测试锁的可重入性
     */
    public void test() {
        RedisLockUtil lockClient = this.lockClient.getLockClient("lock");
        lockClient.lock();
        lockClient.unlock();
    }
}    

总结

加锁:

  1. setnx:独占排他 死锁、不可重入、原子性
  2. set kv ex 30nx:独占排他、死锁不可重入
  3. hash +lua脚本:可重入锁
    1. 判断锁是否被占用(exists),如果没有被占用则直接获取锁(hset/hincrby) 并设置过期时间(expire)
    2. 如果锁被占用,则判断是否当前线程占用的(hexists),如果是则重入(hincrby) 并重置过期时间(expire)
    3. 3.否则获取锁失败,将来代码中重试
  4. 重试:递归 循环
  5. Timer定时器+lua脚本:实现锁的自动续期
    判断锁是否自己的锁(hexists),如果是自己的锁则执行(expire)设置过期时间

解锁:

  1. del:导致误删
  2. 先判断再删除同时保证原子性: lua脚本
  3. hash +lua脚本:可重入 T
    1. 判断当前线程的锁是否存在,不存在则返回nil,将来在代码中抛出异常
    2. 存在则直接减1 (hincrby -1),判断减1后的值是否为0,为0则释放锁(del),并返回1
    3. 不为0,则返回0
相关推荐
敖云岚27 分钟前
【Redis】分布式锁的介绍与演进之路
数据库·redis·分布式
LUCIAZZZ1 小时前
HikariCP数据库连接池原理解析
java·jvm·数据库·spring·springboot·线程池·连接池
我在北京coding1 小时前
300道GaussDB(WMS)题目及答案。
数据库·gaussdb
正在努力Coding1 小时前
kafka(windows)
分布式·kafka
小Tomkk1 小时前
阿里云 RDS mysql 5.7 怎么 添加白名单 并链接数据库
数据库·mysql·阿里云
明月醉窗台2 小时前
qt使用笔记二:main.cpp详解
数据库·笔记·qt
让我上个超影吧3 小时前
黑马点评【基于redis实现共享session登录】
java·redis
沉到海底去吧Go3 小时前
【图片自动识别改名】识别图片中的文字并批量改名的工具,根据文字对图片批量改名,基于QT和腾讯OCR识别的实现方案
数据库·qt·ocr·图片识别自动改名·图片区域识别改名·pdf识别改名
老纪的技术唠嗑局3 小时前
重剑无锋,大巧不工 —— OceanBase 中的 Nest Loop Join 使用技巧分享
数据库·sql
未来之窗软件服务4 小时前
JAVASCRIPT 前端数据库-V6--仙盟数据库架构-—-—仙盟创梦IDE
数据库·数据库架构·仙盟创梦ide·东方仙盟·东方仙盟数据库