Redis实现分布式锁

对于多线程安全问题,在单机模式下,我们常常使用乐观锁和悲观锁解决

  • 悲观锁:认为线程安全问题一定会发生,因此在操作数据之前先获取锁,确保线程串行执行

    • Synchronized 和 lock 是悲观锁的典型代表
  • 乐观锁:认为线程安全问题不一定会发生,所以并不真正加锁,只是在更新数据时去判断有没有其他线程对数据进行修改,如果没有修改,则认为线程是安全的,而后才更新数据,否则认为线程是不安全的

    • 乐观锁常见的实现方式是依靠版本号,共享的实体存在一个版本号字段version,每次操作数据会对版本号+1,提交回数据时,先去校验是否比之前的版本大1 ,如果大1 ,则进行操作成功;
    • 乐观锁的典型代表:cas
    • 乐观锁的核心思想是:在数据更改之前先对当前数据进行校验

乐观锁常用于解决更改数据时的多线程安全问题,悲观锁则更多用于修改数据时的线程安全问题

通过加锁可以解决在单机情况下的多线程安全问题,但是在集群模式下就不行了。 当我们部署了多个tomcat,每个tomcat都有一个属于自己的jvm,那么假设在服务器A的tomcat内部,有两个线程,他们的锁对象是同一个,是可以实现互斥的,但是如果现在是服务器B的tomcat内部,又有两个线程,但是他们的锁对象写的虽然和服务器A一样,但是锁对象却与服务器A的不是同一个,所以线程3和线程4可以实现互斥,但是却无法和线程1和线程2实现互斥,这就是集群环境下,syn锁失效的原因,在这种情况下,我们就需要使用分布式锁来解决这个问题。

什么是分布式锁

分布式锁是一种用于在分布式系统中实现资源的互斥访问的机制。在分布式环境中,多个节点同时访问共享资源时,为了保证数据的一致性和正确性,需要确保同一时间只有一个节点可以对资源进行操作,其他节点需要等待。

常见的实现分布式锁的方式包括:

  • 基于数据库:通过在数据库中创建唯一索引或者使用悲观锁机制来实现分布式锁。
  • 基于缓存:利用分布式缓存(如Redis)的原子性操作和过期时间特性,通过设置一个特定的键值对来实现分布式锁。
  • 基于ZooKeeper:利用ZooKeeper的有序临时节点和Watch机制来实现分布式锁。

此处我们重点讨论基于Redis实现分布式的方法和原理

基于SETNX实现分布式锁

Redis的SETNX命令是一种用于设置键值对的原子性操作。它在键不存在的情况下设置键的值,并返回设置成功与否的结果。

执行SETNX命令时,会进行以下操作:

  • 如果键key不存在,则将键 key 的值设置为 value。
  • 如果键 key 已经存在,则不进行任何操作,返回0。

redis基于setnx实现分布式锁的核心思想:利用setnx方法,将某个键作为锁的标识,通过 SETNX 来尝试获取锁,如果返回1表示获取锁成功,否则表示锁已被其他节点占用。

需要注意的是,由于 SETNX 命令是原子性的,当获取到锁后,需要在适当的时候释放锁,以避免锁的长期占用。

实现分布式锁时需要实现的两个基本方法:

  • 获取锁:
    • 互斥:确保只能有一个线程获取锁
    • 非阻塞:尝试一次,成功返回true,失败返回false

利用setnx方法进行加锁,同时增加过期时间,防止死锁,此方法可以保证加锁和增加过期时间具有原子性

java 复制代码
private static final String KEY_PREFIX="myLock:"
@Override
public boolean tryLock(long timeoutSec) {
    // 获取线程标示
    String threadId = Thread.currentThread().getId()
    // 获取锁
    Boolean success = stringRedisTemplate.opsForValue()
            .setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
    return Boolean.TRUE.equals(success);
}
  • 释放锁:
    • 手动释放
    • 超时释放:获取锁时添加一个超时时间

释放锁,防止删除别人的锁

java 复制代码
public void unlock() {
    //通过del删除锁
    stringRedisTemplate.delete(KEY_PREFIX + name);
}

锁误删

前提:线程1和线程2 操作的是同一把锁(即同一共享资源,key一致) 当持有锁的线程1在锁的内部出现了阻塞,导致他的锁自动释放,这时线程2来尝试获得锁,就拿到了这把锁,然后线程2在持有锁执行过程中,线程1反应过来,继续执行,而线程1执行过程中,走到了删除锁逻辑,由于key一致,此时就会把本应该属于线程2的锁进行删除,这就是锁误删

解决方案 在获取锁时存入线程标示(可以用UUID表示) 在释放锁时先获取锁中的线程标示,判断是否与当前线程标示一致

  • 如果一致则释放锁
  • 如果不一致则不释放锁
java 复制代码
public void unlock() {
    // 获取线程标示
    String threadId = ID_PREFIX + Thread.currentThread().getId();
    // 获取锁中的标示
    String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
    // 判断标示是否一致
    if(threadId.equals(id)) {
        // 释放锁
        stringRedisTemplate.delete(KEY_PREFIX + name);
    }
}

在实际业务中,还存在更为极端的误删情况:线程1持有锁完成业务后,进入删除锁的方法,获取锁标识完成了条件判断,但是此时他的锁到期了,线程1超时释放锁,同时线程2进来,获得锁;当阻塞完成后,线程1会接着往后执行,进行锁删除,相当于条件判断并没有起到作用

这是删锁时的原子性问题,之所以有这个问题,是因为线程1的拿锁,比锁,删锁,实际上并不是原子性的,我们要防止刚才的情况发生,

Lua脚本解决多条命令原子性问题

Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。

Redis提供的调用函数 redis.call('命令名称', 'key', '其它参数', ...)

lua脚本:获取锁中的线程标示,判断是否与指定的标示(当前线程标示)一致,如果一致则释放锁(删除)如果不一致则什么都不做

lua 复制代码
-- 这里的 KEYS[1] 就是锁的key,这里的ARGV[1] 就是当前线程标示
-- 获取锁中的标示,判断是否与当前线程标示一致
if (redis.call('GET', KEYS[1]) == ARGV[1]) then
  -- 一致,则删除锁
  return redis.call('DEL', KEYS[1])
end
-- 不一致,则直接返回
return 0

RedisTemplate中,可以利用execute方法去执行lua脚本

java 复制代码
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
    static {
        UNLOCK_SCRIPT = new DefaultRedisScript<>();
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
        UNLOCK_SCRIPT.setResultType(Long.class);
    }

public void unlock() {
    // 调用lua脚本
    stringRedisTemplate.execute(
            UNLOCK_SCRIPT,
            Collections.singletonList(KEY_PREFIX + name),
            ID_PREFIX + Thread.currentThread().getId());
}
相关推荐
曙曙学编程几秒前
初级数据结构——树
android·java·数据结构
BestandW1shEs6 分钟前
彻底理解消息队列的作用及如何选择
java·kafka·rabbitmq·rocketmq
爱吃烤鸡翅的酸菜鱼9 分钟前
Java算法OJ(8)随机选择算法
java·数据结构·算法·排序算法
码蜂窝编程官方12 分钟前
【含开题报告+文档+PPT+源码】基于SpringBoot+Vue的虎鲸旅游攻略网的设计与实现
java·vue.js·spring boot·后端·spring·旅游
Viktor_Ye28 分钟前
高效集成易快报与金蝶应付单的方案
java·前端·数据库
hummhumm30 分钟前
第 25 章 - Golang 项目结构
java·开发语言·前端·后端·python·elasticsearch·golang
一二小选手35 分钟前
【Maven】IDEA创建Maven项目 Maven配置
java·maven
J老熊40 分钟前
JavaFX:简介、使用场景、常见问题及对比其他框架分析
java·开发语言·后端·面试·系统架构·软件工程
猿java1 小时前
什么是 Hystrix?它的工作原理是什么?
java·微服务·面试
AuroraI'ncoding1 小时前
时间请求参数、响应
java·后端·spring