目录

十九、分布式锁-实现Redis分布式锁版本1

1.interface ILock

2.SimpleRedisLock.java

3.VoucherOrderServiceImpl.java

二十、分布式锁-解决Redis分布式锁误删问题

1.SimpleRedisLock.java

二十一、分布式锁-Lua脚本解决多条命令原子性问题

1.Redis EVAL 使用笔记

2.笔记总结

3.unlock.lua

4.SimpleRedisLock.java

二十二、分布式锁-Redisson快速入门-Redisson设置锁

1.pom.xml

2.application.yaml

3.RedissonConfig.java

4.VoucherOrderServiceImpl.java


十九、分布式锁-实现Redis分布式锁版本1

1.interface ILock

package com.hmdp.utils;

import java.util.concurrent.TimeUnit;

/**
 * 分布式锁接口
 */
public interface ILock {

    /**
     * 尝试获取锁
     * @param key 锁的键
     * @param timeout 超时时间
     * @param timeUnit 时间单位
     * @return 是否获取成功
     */
    boolean tryLock(String key, Integer timeout, TimeUnit timeUnit);

    /**
     * 释放锁
     * @param key 锁的键
     */
    void unlock(String key);

}

2.SimpleRedisLock.java

package com.hmdp.utils;

import cn.hutool.core.util.BooleanUtil;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;

/**
 * 简单Redis分布式锁实现类
 */
@Component
public class SimpleRedisLock implements ILock{

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    // 锁的前缀,用于构建完整的Redis键
    private static final String KEY_PREFIX = "lock:";


    /**
     * 尝试获取锁
     * @param key 锁的键
     * @param timeout 超时时间
     * @param timeUnit 时间单位
     * @return 是否获取成功
     */
    @Override
    public boolean tryLock(String key, Integer timeout, TimeUnit timeUnit) {
        // 获取当前线程的id
        long threadId = Thread.currentThread().getId();
        // 尝试获取锁
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + key, String.valueOf(threadId), timeout, timeUnit);

        return BooleanUtil.isTrue(success);
    }


    /**
     * 释放锁
     * @param key 锁的键
     */
    @Override
    public void unlock(String key) {
        stringRedisTemplate.delete(KEY_PREFIX + key);
    }

}

3.VoucherOrderServiceImpl.java

package com.hmdp.service.impl;

    @Resource
    private SimpleRedisLock simpleRedisLock;

    /**
     * 优惠券秒杀下单
     * @param voucherId 优惠券ID
     * @return Result
     */
    @Override
    public Result seckillVoucher(Long voucherId) {
        // 1.查询优惠券信息
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        // 2.判断秒杀是否开始
        LocalDateTime now = LocalDateTime.now();
        if (voucher.getBeginTime().isAfter(now)) {
            return Result.fail("秒杀尚未开始");
        }
        // 3.判断秒杀是否结束
        if (voucher.getEndTime().isBefore(now)) {
            return Result.fail("秒杀已结束");
        }
        // 4.判断库存是否充足
        if (voucher.getStock() < 1) {
            return Result.fail("库存不足");
        }
        // TODO 实现Redis分布式锁版本
        // 5.1获取当前登录用户id
        Long userId = UserHolder.getUser().getId();
        // 5.2获取锁
        String key = "order:" + userId;  //业务名+用户id
        boolean isLock = simpleRedisLock.tryLock(key, 1200, TimeUnit.SECONDS);  // 超时设置为业务运行的10倍就行,这里是为了测试调高
        // 5.3判断锁是否存在
        if (!isLock){
            // 5.4获取锁失败,返回错误信息或重试
            return Result.fail("不允许重复下单");
        }
        try {
            // 6.创建订单
            // TODO 使用代理对象调用事务方法(不然下面的事务会失效)
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        } catch (IllegalStateException e) {
            throw new RuntimeException(e);
        } finally {
            // 释放锁
            simpleRedisLock.unlock(key);
        }

    }

二十、分布式锁-解决Redis分布式锁误删问题

1.SimpleRedisLock.java

package com.hmdp.utils;

/**
 * 简单Redis分布式锁实现类
 */
@Component
public class SimpleRedisLock implements ILock{

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    // 锁的前缀,用于构建完整的Redis键
    private static final String KEY_PREFIX = "lock:";

    // 线程标识前缀,用于区分不同JVM实例中的线程
    private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";

    /**
     * 尝试获取锁
     * @param key 锁的键
     * @param timeout 超时时间
     * @param timeUnit 时间单位
     * @return 是否获取成功
     */
    @Override
    public boolean tryLock(String key, Integer timeout, TimeUnit timeUnit) {
        // 获取当前线程的标识
        String threadId = ID_PREFIX + Thread.currentThread().getId();

        // 尝试获取锁
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + key, threadId, timeout, timeUnit);

        return BooleanUtil.isTrue(success);
    }


    /**
     * 释放锁
     * @param key 锁的键
     */
    @Override
    public void unlock(String key) {
        // 获取当前线程的标识
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        // 获取锁中的标识
        String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + key);
        // 判断标识是否一致
        if (threadId.equals(id)){
            // 释放锁
            stringRedisTemplate.delete(KEY_PREFIX + key);
        }
    }

}

二十一、分布式锁-Lua脚本解决多条命令原子性问题

1.Redis EVAL 使用笔记

Redis 支持使用 Lua 脚本 来执行一组原子操作,常用命令是 EVAL
语法:

EVAL script numkeys key [key ...] arg [arg ...]
  • script:Lua 脚本
  • numkeys:后面有多少个 key
  • key:传递的 key
  • arg:传递的参数

下面是一个简单的例子 👇


# 1. 使用 EVAL 执行 Lua 脚本,直接 set 一个 key
eval "return redis.call('set','name','jack')" 0

# 查看结果,name 被设置为 "jack"
get name
# 2. 使用 KEYS 和 ARGV 参数,动态传递 key 和 value
# 这里 numkeys = 1,表示有一个 key
# KEYS[1] = name, ARGV[1] = rose
eval "return redis.call('set',KEYS[1],ARGV[1])" 1 name rose

# 查看结果,name 被更新为 "rose"
get name
# 3. 再次执行 Lua 脚本,修改 name 的值为 "shushu"
eval "return redis.call('set',KEYS[1],ARGV[1])" 1 name shushu

# 查看结果,name 被更新为 "shushu"
get name

2.笔记总结

  1. redis.call 用来在 Lua 脚本里调用 Redis 命令。
  2. KEYS 用来传递 key,ARGV 用来传递参数(value 等)。
  3. numkeys 表示有多少个 KEYS 参数,其余都算到 ARGV 里。
  4. 脚本在 Redis 内部执行,具备原子性,不会被打断。

3.unlock.lua

释放锁的业务流程是这样的:

  1. 获取锁中的线程标示
  2. 判断是否与指定的标示(当前线程标示)一致
  3. 如果一致则释放锁(删除)
  4. 如果不一致则什么都不做

这个流程确保了只有拥有正确线程标示的线程才能释放锁,从而避免了其他线程误操作的可能性。

---
--- Generated by EmmyLua(https://github.com/EmmyLua)
--- Created by 20719.
--- DateTime: 2025/8/29 15:47
---

--获取锁中的线程标识
local id = redis.call('get',KEYS[1])
--比较线程标识与锁中的标识是否一致
if(id == ARGV[1]) then
    --释放锁 del key
    return redis.call('del',KEYS[1])
end
return 0

4.SimpleRedisLock.java

package com.hmdp.utils;

/**
 * 简单Redis分布式锁实现类
 */
@Component
public class SimpleRedisLock implements ILock{

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    // 锁的前缀,用于构建完整的Redis键
    private static final String KEY_PREFIX = "lock:";

    // 线程标识前缀,用于区分不同JVM实例中的线程
    private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";

    // Lua脚本用于安全地释放锁,确保只有持有锁的线程才能释放它
    private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;

    // static 代码块用于初始化类级别的静态变量
    static {
        // 初始化解锁Lua脚本
        UNLOCK_SCRIPT = new DefaultRedisScript<>();
        // 设置Lua脚本文件的位置
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
        // 设置Lua脚本的返回值类型
        UNLOCK_SCRIPT.setResultType(Long.class);
    }

    /**
     * 尝试获取锁
     * @param key 锁的键
     * @param timeout 超时时间
     * @param timeUnit 时间单位
     * @return 是否获取成功
     */
    @Override
    public boolean tryLock(String key, Integer timeout, TimeUnit timeUnit) {
        // 获取当前线程的标识
        String threadId = ID_PREFIX + Thread.currentThread().getId();

        // 尝试获取锁
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + key, threadId, timeout, timeUnit);

        return BooleanUtil.isTrue(success);
    }

    /**
     * 释放锁
     * @param key 锁的键
     */
    @Override
    public void unlock(String key) {
        // 获取锁中的标识
        String KEYS = KEY_PREFIX + key;
        // 获取当前线程的标识
        String ARGC= ID_PREFIX + Thread.currentThread().getId();
        // 释放锁
        stringRedisTemplate.execute(UNLOCK_SCRIPT, Collections.singletonList(KEYS),ARGC);
    }

    // /**
    //  * 释放锁
    //  * @param key 锁的键
    //  */
    // @Override
    // public void unlock(String key) {
    //     // 获取当前线程的标识
    //     String threadId = ID_PREFIX + Thread.currentThread().getId();
    //     // 获取锁中的标识
    //     String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + key);
    //     // 判断标识是否一致
    //     if (threadId.equals(id)){
    //         // 释放锁
    //         stringRedisTemplate.delete(KEY_PREFIX + key);
    //     }
    // }

}

二十二、分布式锁-Redisson快速入门-Redisson设置锁

1.pom.xml

        <!--redisson-->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.13.6</version>
        </dependency>

2.application.yaml

其实这个还是原来Redis的配置,引入Redisson时无需更改的

server:
  port: 8081
spring:
  application:
    name: hmdp
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/hmdp?allowPublicKeyRetrieval=true&useSSL=false&serverTimezone=UTC
    username: root
    password: 123456
  redis:
    host: 192.168.200.128 # Redis服务器地址
    port: 6379 # Redis服务器端口
    password:  # Redis服务器密码(如果有的话)
    database: 0 # 指定Redis数据库索引,默认为0
    lettuce:
      pool:
        max-active: 10  # 连接池最大连接数
        max-idle: 10    # 连接池最大空闲连接数
        min-idle: 1     # 连接池最小空闲连接数
        time-between-eviction-runs: 10s  # 空闲连接检测间隔时间
  jackson:
    default-property-inclusion: non_null # JSON处理时忽略非空字段
mybatis-plus:
  type-aliases-package: com.hmdp.entity # 别名扫描包
logging:
  level:
    com.hmdp: debug

3.RedissonConfig.java

虽然 redisson-spring-boot-starter 可以自动装配,省去手写 Config 配置类,但在某些场景下可能会与 Spring 自带的 Redis 配置产生冲突,因此一般不推荐使用。

package com.hmdp.config;

//Redisson客户端的配置
@Configuration
public class RedissonConfig {

    @Value("${spring.redis.host:localhost}")
    private String host;

    @Value("${spring.redis.port:6379}")
    private int port;

    @Value("${spring.redis.password:}") // 如果没密码就是空
    private String password;

    @Value("${spring.redis.database:0}")
    private int database;

    @Bean
    public RedissonClient redissonClient(){
        // 创建Redisson配置对象
        Config config = new Config();

        // 配置单节点模式的Redis连接信息
        config.useSingleServer()
                .setAddress("redis://" + host + ":" + port)  // 设置Redis服务器地址和端口
                .setDatabase(database);                      // 设置使用的数据库索引

        // 如果设置了密码,则配置密码
        if (password != null && !password.isEmpty()){
            config.useSingleServer()
                    .setPassword(password);  // 设置Redis访问密码
        }

        // 根据配置创建并返回Redisson客户端实例
        return Redisson.create(config);
    }

}

4.VoucherOrderServiceImpl.java

使用Redisson获取锁,参数分别为:等待时间、过期时间、时间单位

过期时间一定一定一定要默认,这样才能有看门狗续期机制

boolean isLock = lock.tryLock(-1,TimeUnit.SECONDS);

package com.hmdp.service.impl;

@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {

    @Resource
    private ISeckillVoucherService seckillVoucherService;

    @Resource
    private RedisIdWorker redisIdWorker;

    @Resource
    private SimpleRedisLock simpleRedisLock;

    @Resource
    private RedissonClient redissonClient;

    /**
     * 优惠券秒杀下单
     * @param voucherId 优惠券ID
     * @return Result
     */
    @Override
    public Result seckillVoucher(Long voucherId) throws InterruptedException {
        // 1.查询优惠券信息
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        // 2.判断秒杀是否开始
        LocalDateTime now = LocalDateTime.now();
        if (voucher.getBeginTime().isAfter(now)) {
            return Result.fail("秒杀尚未开始");
        }
        // 3.判断秒杀是否结束
        if (voucher.getEndTime().isBefore(now)) {
            return Result.fail("秒杀已结束");
        }
        // 4.判断库存是否充足
        if (voucher.getStock() < 1) {
            return Result.fail("库存不足");
        }
        // TODO 实现Redis分布式锁版本
        // 5.1获取当前登录用户id
        Long userId = UserHolder.getUser().getId();
        // 5.2获取锁
        // String key = "order:" + userId;  //业务名+用户id
        // boolean isLock = simpleRedisLock.tryLock(key, 1200, TimeUnit.SECONDS);  // 超时设置为业务运行的10倍就行,这里是为了测试调高
        // TODO 使用Redisson创建锁对象
        RLock lock = redissonClient.getLock("lock:order:" + userId);
        // TODO 使用Redisson获取锁,参数分别为:等待时间、过期时间、时间单位
        // -1表示无限等待,直到获取到锁为止
        // 过期时间默认,这样才能有看门狗续期机制
        boolean isLock = lock.tryLock(-1,TimeUnit.SECONDS);
        // 5.3判断锁是否存在
        if (!isLock){
            // 5.4获取锁失败,返回错误信息或重试
            return Result.fail("不允许重复下单");
        }
        try {
            // 6.创建订单
            // TODO 使用代理对象调用事务方法(不然下面的事务会失效)
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        } catch (IllegalStateException e) {
            throw new RuntimeException(e);
        } finally {
            // TODO 使用Redisson释放锁
            lock.unlock();
        }
    }

    // 6.创建订单(方法)
    @Transactional
    public Result createVoucherOrder(Long voucherId) {
        // 5.一人一单
        // 获取当前登录用户id
        Long userId = UserHolder.getUser().getId();
        // 5.1查询订单
        long count = lambdaQuery()
                .eq(VoucherOrder::getUserId, userId)
                .eq(VoucherOrder::getVoucherId, voucherId)
                .count();
        // 5.2判断是否存在
        if (count > 0){
            // 5.3用户已经购买过了
            return Result.fail("领取次数过多");
        }
        // 6.扣减库存
        boolean success = seckillVoucherService.lambdaUpdate()
                .setSql("stock = stock - 1")
                .eq(SeckillVoucher::getVoucherId, voucherId)
                .gt(SeckillVoucher::getStock, 0)  // TODO 乐观锁解决库存超卖问题
                .update();
        if (!success){
            return Result.fail("库存不足");
        }
        // 7、创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        // 7.1设置id
        long id = redisIdWorker.nextId("order");
        voucherOrder.setId(id);
        // 7.2设置用户id
        voucherOrder.setUserId(userId);
        // 7.3设置优惠券id
        voucherOrder.setVoucherId(voucherId);
        // 保存到数据库
        save(voucherOrder);
        // 8.返回订单id
        return Result.ok(voucherId);
    }

}
Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐