基于Redis的分布式锁

引入

在jvm内部只有一个锁监视器,所以只有一个线程可以获取锁,可以实现线程间的互斥

但是,当有多个jvm的时候,就会有多个锁监视器,就会有多个线程获取到锁,这样就没有办法实现多jvm进程之间的互斥了

要解决这个问题,就不能再用jvm的锁监视器了,而是在jvm外有一个共同使用的锁监视器

分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁

基于redis的分布式锁实现思路

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

1.获取锁

  • 互斥:确保只能有一个线程获取锁

  • 非阻塞:尝试一次,成功返回true,失败返回false

    添加锁,NX是互斥,EX是设置超时时间

    SET lock thread1 NX EX 10

2.释放锁

  • 手动释放

  • 超时释放:获取锁时添加一个超时时间

    释放锁,删除即可

    DEL key

流程图:

代码实现1.0

接口:

java 复制代码
public interface ILock {

    /**
     * 尝试获取锁
     * @param timeoutSec 锁的过期时间,单位秒
     * @return true代表获取锁成功,false代表获取锁失败
     */
    boolean tryLock(long timeoutSec);

    /**
     * 释放锁
     */
    void unLock();

}

工具类:

java 复制代码
public class SimpleRedisLock implements ILock{

    private String name;
    private StringRedisTemplate stringRedisTemplate;

    public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
        this.name = name;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    private static final String KEY_PREFIX = "lock:";

    @Override
    public boolean tryLock(long timeoutSec) {
        // 获取线程标识
        String threadId = Thread.currentThread().getId() + "";
        // 获取锁
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
        // 防止拆箱出现空指针的情况(Boolean->boolean),所以用下面这个表达式,而不是直接返回success
        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unLock() {
        // 释放锁
        stringRedisTemplate.delete(KEY_PREFIX + name);
    }
}

实现类:

java 复制代码
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {

    @Autowired
    private ISeckillVoucherService seckillVoucherService;

    @Autowired
    private RedisIdWorker redisIdWorker;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public Result seckillVoucher(Long voucherId) {
        // 1.查询优惠券
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        // 2.判断秒杀是否开始
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
            // 尚未开始
            return Result.fail("秒杀尚未开始");
        }
        // 3.判断秒杀是否结束
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            // 已经结束
            return Result.fail("秒杀已经结束");
        }
        // 4.判断库存是否充足
        if (voucher.getStock() < 1) {
            // 库存不足
            return Result.fail("库存不足");
        }
        Long userId= UserHolder.getUser().getId();
        // 创建锁对象
        SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
        // 获取锁
        boolean isLock = lock.tryLock(1200L);
        // 判断是否获取锁成功
        if (!isLock) {
            // 获取锁失败,返回错误信息
            return Result.fail("不允许重复下单");
        }
        try {
            // 获取代理对象(事务)
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        } catch (IllegalStateException e) {
            throw new RuntimeException(e);
        }finally {
            // 释放锁
            lock.unLock();
        }
    }

    @Transactional
    public Result createVoucherOrder(Long voucherId) {
        // 5.一人一单
        Long userId = UserHolder.getUser().getId();
        // 5.1.查询订单
        int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
        // 5.2.判断是否存在
        if (count > 0) {
            // 存在,不能重复下单
            return Result.fail("不能重复下单");
        }
        // 6.扣减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock=stock-1")
                .eq("voucher_id", voucherId)
                .gt("stock", 0)
                .update();
        if (!success) {
            // 扣减失败
            return Result.fail("库存不足");
        }
        // 7.创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        // 7.1 订单id
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        // 7.2 用户id
        voucherOrder.setUserId(userId);
        // 7.3 代金券id
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder);
        // 8.返回订单id
        return Result.ok(orderId);
    }
}

存在问题:

假设线程1获取到锁了,但是业务阻塞了,于是锁超时释放了,这时线程2获取到了锁,正在执行自己业务的过程中,线程1的业务完成了,这个时候线程1就会把线程2的锁给释放掉,这时线程3就能获取到锁,导致线程2和线程3同时获取到了锁,出现了安全问题

解决方法:

释放锁时需要获取锁标识并判断是否一致,一致才能释放锁

流程图:

代码实现2.0

工具类

java 复制代码
public class SimpleRedisLock implements ILock{

    private String name;
    private StringRedisTemplate stringRedisTemplate;

    public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
        this.name = name;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    private static final String KEY_PREFIX = "lock:";
    // 使用hutool包的UUID来唯一标识jvm进程
    private static final String ID_PREFIX = UUID.randomUUID().toString(true)+"-";

    @Override
    public boolean tryLock(long timeoutSec) {
        // 获取线程标识
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        // 获取锁
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
        // 防止拆箱出现空指针的情况(Boolean->boolean),所以用下面这个表达式,而不是直接返回success
        return Boolean.TRUE.equals(success);
    }

    @Override
    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在获取锁标识并判断是一致的时候,刚要释放锁,突然阻塞了,导致锁超时释放,这时线程2获取到了锁,开始执行自己的业务。。。(接下来与上一个问题一样)

解决思路:把判断和释放做成一个原子性的动作。

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

Lua

Lua是一种编程语言,它的基本语法可以参考网站:Lua 教程 | 菜鸟教程

redis提供的调用函数

语法:

java 复制代码
# 执行redis命令
redis.call('命令名称', 'key', '其他参数', ...)

例如,我们要执行set name jack,则脚本是这样:

java 复制代码
# 执行set name jack
redis.call('set', 'name', 'jack')

例如,我们要先执行set name jack,再执行get name,则脚本如下:

java 复制代码
# 先执行set name jack
redis.call('set', 'name', 'jack')
# 在执行get name
local name = redis.call('get', 'name')
# 返回
return name

调用脚本

写好脚本以后,需要用Redis命令来调用脚本,调用脚本的常见命令如下:

java 复制代码
EVAL script numkeys key [key ...] arg [arg ...]

例如,我们要执行redis.call('set','name',jack')这个脚本,语法如下:

java 复制代码
# 调用脚本
EVAL "return redis.call('set', 'name', 'jack')" 0

双引号内为脚本内容,后面的数字为脚本需要的key类型的参数个数

如果脚本中的key、value不想写死,可以作为参数传递。key类型参数会放入KEYS数组,其它参数会放入ARGV数组,在脚本中可以从KEYS和ARGV数组获取这些参数:

java 复制代码
# 调用脚本
EVAL "return redis.call('set', KEYS[1], ARGV[1])" 1 name Rose

name对应KEYS[1],Rose对应ARGV[1]

在Lua中,第一个元素的下标为1

代码实现3.0

释放锁的Lua脚本

Lua 复制代码
-- 获取锁中的线程标识
local id = redis.call("GET", KEYS[1])
-- 比较线程标识与锁中的标识是否一致
if (id == ARGV[1]) then
    -- 一致则删除锁,释放锁
    return redis.call("DEL", KEYS[1])
else
    -- 不一致则直接返回0,表示未释放锁
    return 0
end

首先安装Lua的插件

然后在resources下新建一个Lua脚本写入这段代码

工具类

java 复制代码
public class SimpleRedisLock implements ILock{

    private String name;
    private StringRedisTemplate stringRedisTemplate;

    public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
        this.name = name;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    private static final String KEY_PREFIX = "lock:";
    private static final String ID_PREFIX = UUID.randomUUID().toString(true)+"-";
    // 声明静态常量:脚本对象是类级别的,全局唯一且不可修改
    private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
    // 静态代码块:类加载时初始化这个常量
    static {
        // 第一步:创建 Redis Lua 脚本对象实例(只 new 一次)
        UNLOCK_SCRIPT = new DefaultRedisScript<>();
        // 第二步:指定 Lua 脚本文件的路径(加载脚本内容,只加载一次)
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
        // 第三步:指定脚本执行后的返回值类型(只配置一次)
        UNLOCK_SCRIPT.setResultType(Long.class);
    }

    @Override
    public boolean tryLock(long timeoutSec) {
        // 获取线程标识
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        // 获取锁
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
        // 防止拆箱出现空指针的情况(Boolean->boolean),所以用下面这个表达式,而不是直接返回success
        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unLock() {
        // 调用Lua脚本
        stringRedisTemplate.execute(
                UNLOCK_SCRIPT,
                Collections.singletonList(KEY_PREFIX + name),
                ID_PREFIX + Thread.currentThread().getId());
    }

}

这样就解决了上述问题,但是还可以优化

Redisson框架

Redisson是一个在Redis的基础上实现的Java驻内存数据网格。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。

官网地址: Redisson | Valkey & Redis Java client. Ultimate Real-Time Data Platform

GitHub地址: redisson/redisson: Redisson - Valkey & Redis Java client. Real-Time Data Platform. Sync/Async/RxJava/Reactive API. Over 50 Valkey and Redis based Java objects and services: Set, Multimap, SortedSet, Map, List, Queue, Deque, Semaphore, Lock, AtomicLong, Map Reduce, Bloom filter, Spring, Tomcat, Scheduler, JCache API, Hibernate, RPC, local cache..

Redisson入门

1.引入依赖

XML 复制代码
<dependency>
  <groupId>org.redisson</groupId>
  <artifactId>redisson</artifactId>
  <version>3.13.6</version>
</dependency>

2.配置Redisson客户端

java 复制代码
@Configuration
public class RedisConfig {
    @Bean
    public RedissonClient redissonClient() {
        // 配置类
        Config config = new Config();
        // 添加redis地址,这里添加了单点的地址,也可以使用config.useClusterServers()添加集群地址
        config.useSingleServer().setAddress("redis://192.168.100.128:6379").setPassowrd("1234") ;
        // 创建客户端
        return Redisson.create(config);
    }
}

3.使用Redisson的分布式锁

java 复制代码
@Resource
private Redissonclient redissonclient;

@Test
void testRedisson() throws InterruptedException {
    //获取锁(可重入),指定锁的名称
    RLock lock = redissonclient.getLock("anyLock");
    // 尝试获取锁,参数分别是:获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位
    boolean isLock = lock.tryLock(1, 10, TimeUnit.SEcoNDS);
    // 判断释放获取成功
    if(isLock){
        try {
            System.out.println("执行业务");
        }finally {
            // 释放锁
            Lock.unlock();
        }
    }
}

Redisson功能

解决了不可重入、不可重试、超时释放、主从一致性的问题

  • 不可重入:同一个线程无法多次获取同一把锁
  • 不可重试 :获取锁只尝试一次就返回false,没有重试机制
  • 超时释放:锁超时释放虽然可以避免死锁,但如果是业务执行耗时较长,也会导致锁释放,存在安全隐患
  • 主从一致性:如果Redis提供了主从集群,主从同步存在延迟,当主宕机时,如果从并同步主中的锁数据,则会出现锁实现
相关推荐
数据与后端架构提升之路1 天前
Seata 全景拆解:AT、TCC、Saga 该怎么选?告别“一把梭”的架构误区
分布式·架构
蓝眸少年CY1 天前
什么是Hadoop
大数据·hadoop·分布式
不做码农好多年,该何去何从。1 天前
zookeeper是什么可以做什么?
分布式·zookeeper·云原生
talle20211 天前
Spark分布式计算框架介绍
大数据·分布式·spark·rdd
【赫兹威客】浩哥1 天前
【赫兹威客】Hadoop完全分布式克隆文件部署教程
大数据·hadoop·分布式
编程彩机1 天前
互联网大厂Java面试:从Spring Boot到分布式缓存的技术场景解析
java·redis·分布式·缓存·大厂面试·技术解析·sprint boot
蓝眸少年CY1 天前
(第十三篇)spring cloud之Sleuth分布式链路跟踪
分布式·spring·spring cloud
德彪稳坐倒骑驴1 天前
Spark面试准备
大数据·分布式·spark
小北方城市网2 天前
Spring Cloud Gateway 生产级实践:高可用架构、灰度发布与故障排查
spring boot·redis·分布式·缓存·架构·wpf
奥特曼_ it2 天前
【Spark+Hadoop】基于spark+hadoop游戏评论数据分析可视化大屏(完整系统源码+数据库+开发笔记+详细部署教程+虚拟机分布式启动教程)✅
hadoop·分布式·spark