基于Redis实现优惠券秒杀——第3期(分布式锁-Redisson)

建议先看完第1期和第2期:

基于Redis实现优惠券秒杀------第1期(解决超卖问题、一人一单问题)-CSDN博客

基于Redis实现优惠券秒杀------第2期(分布式锁)-CSDN博客

上一期我们说到,使用分布式锁可能存在一下问题:

  • 基于SETNX实现的分布式锁存在以下问题
    1. 重入问题
      • 重入问题是指获取锁的线程,可以再次进入到相同的锁的代码块中,可重入锁的意义在于防止死锁,例如在HashTable这样的代码中,它的方法都是使用synchronized修饰的,加入它在一个方法内调用另一个方法,如果此时是不可重入的,那就死锁了。所以可重入锁的主要意义是防止死锁,我们的synchronized和Lock锁都是可重入的
    2. 不可重试
      • 我们编写的分布式锁只能尝试一次,失败了就返回false,没有重试机制。但合理的情况应该是:当线程获取锁失败后,他应该能再次尝试获取锁
    3. 超时释放
      • 我们在加锁的时候增加了TTL,这样我们可以防止死锁,但是如果卡顿(阻塞)时间太长,也会导致锁的释放。虽然我们采用Lua脚本来防止删锁的时候,误删别人的锁,但现在的新问题是没锁住,也有安全隐患
    4. 主从一致性
      • 如果Redis提供了主从集群,那么当我们向集群写数据时,主机需要异步的将数据同步给从机,万一在同步之前,主机宕机了(主从同步存在延迟,虽然时间很短,但还是发生了),那么又会出现死锁问题

Redisson入门

  1. 导入依赖
java 复制代码
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.13.6</version>
</dependency>

2.配置Redisson客户端,在config包下新建RedissonConfig

java 复制代码
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RedissonConfig {
    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer()
            .setAddress("redis://101.XXX.XXX.160:6379")
            .setPassword("root");
        return Redisson.create(config);
    }
}

3.使用Redisson的分布式锁

java 复制代码
@Resource
private RedissonClient redissonClient;

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

4.替换我们上一期自己写的分布式锁

java 复制代码
@Resource
private RedissonClient redissonClient;
@Override
public Result seckillVoucher(Long voucherId) {
    LambdaQueryWrapper<SeckillVoucher> queryWrapper = new LambdaQueryWrapper<>();
    //1. 查询优惠券
    queryWrapper.eq(SeckillVoucher::getVoucherId, voucherId);
    SeckillVoucher seckillVoucher = seckillVoucherService.getOne(queryWrapper);
    //2. 判断秒杀时间是否开始
    if (LocalDateTime.now().isBefore(seckillVoucher.getBeginTime())) {
        return Result.fail("秒杀还未开始,请耐心等待");
    }
    //3. 判断秒杀时间是否结束
    if (LocalDateTime.now().isAfter(seckillVoucher.getEndTime())) {
        return Result.fail("秒杀已经结束!");
    }
    //4. 判断库存是否充足
    if (seckillVoucher.getStock() < 1) {
        return Result.fail("优惠券已被抢光了哦,下次记得手速快点");
    }
    Long userId = UserHolder.getUser().getId();
    RLock redisLock = redissonClient.getLock("order:" + userId);
    boolean isLock = redisLock.tryLock();
    if (!isLock) {
        return Result.fail("不允许抢多张优惠券");
    }
    try {
        IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
        return proxy.createVoucherOrder(voucherId);
    } finally {
        redisLock.unlock();
    }
}

Redisson可重入锁原理

为了保证原子性,所以流程图中的业务逻辑也是需要我们用Lua来实现的

  • 获取锁的逻辑
Lua 复制代码
local key = KEYS[1]; -- 锁的key
local threadId = ARGV[1]; -- 线程唯一标识
local releaseTime = ARGV[2]; -- 锁的自动释放时间
-- 锁不存在
if (redis.call('exists', key) == 0) then
    -- 获取锁并添加线程标识,state设为1
    redis.call('hset', key, threadId, '1');
    -- 设置锁有效期
    redis.call('expire', key, releaseTime);
    return 1; -- 返回结果
end;
-- 锁存在,判断threadId是否为自己
if (redis.call('hexists', key, threadId) == 1) then
    -- 锁存在,重入次数 +1,这里用的是hash结构的incrby增长
    redis.call('hincrby', key, thread, 1);
    -- 设置锁的有效期
    redis.call('expire', key, releaseTime);
    return 1; -- 返回结果
end;
return 0; -- 代码走到这里,说明获取锁的不是自己,获取锁失败
  • 释放锁的逻辑
java 复制代码
local key = KEYS[1];
local threadId = ARGV[1];
local releaseTime = ARGV[2];
-- 如果锁不是自己的
if (redis.call('HEXISTS', key, threadId) == 0) then
    return nil; -- 直接返回
end;
-- 锁是自己的,锁计数-1,还是用hincrby,不过自增长的值为-1
local count = redis.call('hincrby', key, threadId, -1);
-- 判断重入次数为多少
if (count > 0) then
    -- 大于0,重置有效期
    redis.call('expire', key, releaseTime);
    return nil;
else
    -- 否则直接释放锁
    redis.call('del', key);
    return nil;
end;

下一期我们继续来讲:

基于Redis实现优惠券秒杀--秒杀优化

相关推荐
风筝超冷23 分钟前
网页版部署MySQL + Qwen3-0.5B + Flask + Dify 工作流部署指南
数据库·mysql·flask
JAVA百练成神25 分钟前
Java引用RabbitMQ快速入门
java·rabbitmq·java-rabbitmq
木木子999925 分钟前
WITH在MYSQL中的用法
数据库·mysql
麻芝汤圆31 分钟前
在 Sheel 中运行 Spark:开启高效数据处理之旅
大数据·前端·javascript·hadoop·分布式·ajax·spark
元亓亓亓33 分钟前
Java后端开发day42--IO流(二)--字符集&字符流
java·开发语言
一刀到底2111 小时前
idea内存过低 设置 Maximum Heap Size 终极解决方案
java·ide·intellij-idea
lix的小鱼1 小时前
在sheel中运行Spark
大数据·分布式·spark
代码的奴隶(艾伦·耶格尔)1 小时前
RabbitMq(尚硅谷)
分布式·rabbitmq
Aaaa小嫒同学2 小时前
spark local模式
大数据·分布式·spark
IvanCodes2 小时前
二、Hadoop狭义和广义的理解
大数据·hadoop·分布式