Redis分布式锁实现解析 - 结合秒杀业务

秒杀业务流程(实现一人一单): 

1. 检查秒杀是否已经开始 ,或者秒杀活动是否结束

2. 判断库存是否充足

3. 扣减库存

4. 创建订单

5.返回 订单ID

事务提交前和提交后释放锁的问题。

1. 

 @Transactional
    public Result createVoucherOrder1(Long voucherId) {
        // 5.一人一单
        Long userId = UserHolder.getUser().getId();

        // 创建锁对象 获取 分布式锁
        SimpleRedisLock redisLock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
        // 尝试获取锁
        boolean isLock = redisLock.tryLock(1200);
        // 判断
        if(!isLock){
            // 获取锁失败,直接返回失败或者重试
            return Result.fail("不允许重复下单!");
        }

        try {
            // 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") // set stock = stock - 1
                    .eq("voucher_id", voucherId).gt("stock", 0) // where id = ? and 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);
            // 7.返回订单id
            return Result.ok(orderId);
        } finally {
            // 释放锁
            redisLock.unlock();
        }

    }

 2. 获取事务的代理对象,防止事务的失效,

 @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 redisLock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
        RLock lock = redissonClient.getLock("lock:order"+userId);
        // 获取锁对象
        boolean isLock = lock.tryLock();

        // 获取锁对象
//       boolean isLock = redisLock.tryLock(1200);
       // 判断是否 获取锁 成功
       if(!isLock){
          // 一个人只运行一单 2011f4dd7cd042678b5ae1bcb8c95fdb-76
          return Result.fail("1200");
       }
  设置线程互斥  创建订单业务 的 实现
//
        try {
            synchronized (userId.toString().intern()) {
                // 获取代理对象 (事务)   
                //  这里涉及到 Spring事务失效的几种可能性之一  使用代理对象 进行解决
                IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
                return proxy.createVoucherOrder(voucherId);
            }
        }finally {
           // 锁的释放
           lock.unlock();
//           redisLock.unlock();
       }
   }

同时暴露代理对象

package com.hmdp;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.scheduling.annotation.EnableAsync;

@MapperScan("com.hmdp.mapper")
@EnableAspectJAutoProxy(exposeProxy = true) // 暴露与aop 实现事务有关的代理对象
@SpringBootApplication
@EnableAsync
public class HmDianPingApplication {
    public static void main(String[] args) {
        SpringApplication.run(HmDianPingApplication.class);
    }
}

解决超卖问题: 超卖问题是典型的多线程安全问题,针对这一问题的常见解决方案就是加锁:

加锁方案:

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

乐观锁:认为线程安全问题不一定会发生,因此不加锁,只是在更新数据时去判断有没有其它线程对数据做了修改。如果没有修改则认为是安全的,自己才更新数据。如果已经被其它线程修改说明发生了安全问题,此时可以重试或异常。

分布式锁: 使用setnx 实现 多jvm 的锁监视器,(多进程可见)

redis 实现 分布式锁的操作:

# 获取锁的操作  (ex: 加上过期时间) 非阻塞方式的等待
set lock thread1 nx ex 10

# 释放锁

del lock

分布式锁的实现方案

1.   有阻塞方案 (使用重试机制)


    /**
     *  阻塞的分布式锁
     * @param name
     * @param expire
     * @param timeout 重试时间限制
     * @return
     */
    private boolean Lock(String name,long expire,long timeout){

    long startTime= System.currentTimeMillis();

    boolean ans = false;
     do{
         ans = tryLock2(name,expire);
         if(!ans) {
             if (System.currentTimeMillis() - startTime > timeout) {
                 break;
             }

             try {
                 Thread.sleep(50);
             } catch (InterruptedException e) {
                 throw new RuntimeException(e);
             }
         }
     }while (!ans) ;

        return false;
    }

2. 无阻塞方案

   /**
     * 获取 互斥锁
     *   无阻塞的分布式锁
     * @param key
     * @return
     */
    private boolean tryLock(String key) {
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

1-1 .Redis锁的实现 

   第一版本:  使用 stringRestTemplate 实现 分布式锁


    /**
     * 秒杀业务
     *   1. 判断用户是否可以进行秒杀
     *   2. 扣减优惠券的库存 (扣库存)
     *      秒杀时间的 判断
     *  3. 将用户优惠券的信息写入到订单 (创建订单)  --> 进行订单的创健
     *   一人一单的判断  是否买过
     *       使用分布式锁 获取成功
     * */
    @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();
       // 创建锁对象 获取 分布式锁  将 业务标识和 userId 的 拼接作为锁的 key
        SimpleRedisLock redisLock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
        // 过期时间
        boolean isLock = redisLock.tryLock(1200);
         
//        // 2. 使用 Redission的方式获取锁
//        RLock lock = redissonClient.getLock("lock:order"+userId);
//        boolean isLock = lock.tryLock();

        // 获取锁对象
//       boolean isLock = redisLock.tryLock(1200);
       // 判断是否 获取锁 成功
       if(!isLock){
          // 一个人只允许下一单
          return Result.fail("一个人只允许下一单 ");
       }
  设置线程互斥  创建订单业务 的 实现
//
        try {
            synchronized (userId.toString().intern()) {
                // 获取代理对象 (事务)
                //  这里涉及到 Spring事务失效的几种可能性之一  使用代理对象 进行解决
                IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
                return proxy.createVoucherOrder(voucherId);
            }
        }finally {
           // 锁的释放
            redisLock.unlock();
//           lock.unlock();
       }
   }
1- 2. 第一版本的分布式锁会出现的问题。
      在释放锁时可能会出现误删的问题 

    

代码如下:


import cn.hutool.core.lang.UUID;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;

import java.util.Collections;
import java.util.concurrent.TimeUnit;


/**
 * ILock  redis 锁机制  分布式锁的实现
 * */
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:";
    // 使用 UUID 拼接线程ID
    private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
     // 定义锁的lua 脚本   返回值类型为 指定泛型
    private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
    static {
        // redis 的操作
        UNLOCK_SCRIPT = new DefaultRedisScript<>();
        // 从  classPath中查找并读取 lua脚本内容
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
        UNLOCK_SCRIPT.setResultType(Long.class);
    }

    /**
     * .
     *
     * @author zgy
     * @date  2:43
     * @param timeoutSec
     * @return 返回 是否
     * @methodName tryLock
     *  改进后的分布式锁的实现方案
     **/
    @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);
        return Boolean.TRUE.equals(success);
    }

    /**
     * 释放锁的逻辑 通过 lua脚本进行优化
     */
//    @Override
//    public void unlock() {
//        // 调用lua脚本  保证判断删除 锁 时 操作的  原子性
//        stringRedisTemplate.execute( // 判断锁是否是本线程创建的
//                UNLOCK_SCRIPT,
//                Collections.singletonList(KEY_PREFIX + name),
//                // 即 线程标示
//                ID_PREFIX + Thread.currentThread().getId());
//    }

    /*

     分布式锁情况下 释放锁的逻辑
     */
     @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-3: Redis分布式锁的遇到的问题 : 进行了锁标识的判断,防止了锁的误删问题,此时仍然有可能会在Full GC 时出现 业务代码执行的阻塞导致锁的超时释放问题。此时需要做的是保证判断锁标识的操作和释放锁的操作的原子性。

使用lua脚本命令对Redis锁的释放逻辑进行优化,保证判断线程标识和删除锁操作的原子性操作。

-- 释放锁的 lua 脚本
if (redis.call('GET',keys[1]==argv[1])) then
   -- 删除 第一个键值对
	return redis.call('del',keys[1])
end
return 0;

判断和删除是原子性的操作,

 /**
     * 释放锁的逻辑 通过 lua脚本进行优化
     */
    @Override
    public void unlock() {
        // 调用lua脚本  保证判断删除 锁 时 操作的  原子性
        stringRedisTemplate.execute( // 判断锁是否是本线程创建的
                UNLOCK_SCRIPT,
                // 指定锁的key
                Collections.singletonList(KEY_PREFIX + name),
                // 即 线程标示
                ID_PREFIX + Thread.currentThread().getId());
    }

上述分布式锁的实现存在的问题。

1. 不可重入

2. 不可重试

3. 超时释放

4. 主从一致

 

2  Redission实现 分布式的可重入锁机制.及实现原理

 /**
     * 释放锁的逻辑 通过 lua脚本进行优化
     */
    @Override
    public void unlock() {
        // 调用lua脚本  保证判断删除 锁 时 操作的  原子性
        stringRedisTemplate.execute( // 判断锁是否是本线程创建的
                UNLOCK_SCRIPT,
                // 指定锁的key
                Collections.singletonList(KEY_PREFIX + name),
                // 即 线程标示
                ID_PREFIX + Thread.currentThread().getId());
    }

Redission配置类


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;

import static org.apache.tomcat.jni.SSL.setPassword;

@Configuration
public class RedissonConfig {

    @Bean
    public RedissonClient redissonClient(){
        // 配置
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379")
         // 当 密码 没有时的情况 不用设置
         .setPassword("132132");
        // 创建RedissonClient对象
        return Redisson.create(config);
    }
}

使用 Redission对一人一单业务进行改造,实现分布式锁方案


    /**
     * 秒杀业务
     *   1. 判断用户是否可以进行秒杀
     *   2. 扣减优惠券的库存 (扣库存)
     *      秒杀时间的 判断
     *  3. 将用户优惠券的信息写入到订单 (创建订单)  --> 进行订单的创健
     *   一人一单的判断  是否买过
     *       使用分布式锁 获取成功
     * */
    @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();
       // 创建锁对象 获取 分布式锁  将 业务标识和 userId 的 拼接作为锁的 key
//        SimpleRedisLock redisLock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
        // 过期时间
//        boolean isLock = redisLock.tryLock(1200);
         
//        // 2. 使用 Redission的方式获取锁
        RLock lock = redissonClient.getLock("lock:order"+userId);
        boolean isLock = lock.tryLock();

        // 获取锁对象
//       boolean isLock = redisLock.tryLock(1200);
       // 判断是否 获取锁 成功
       if(!isLock){
          // 一个人只允许下一单
          return Result.fail("一个人只允许下一单 ");
       }
  设置线程互斥  创建订单业务 的 实现
//
        try {
            synchronized (userId.toString().intern()) {
                // 获取代理对象 (事务)
                //  这里涉及到 Spring事务失效的几种可能性之一  使用代理对象 进行解决
                IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
                return proxy.createVoucherOrder(voucherId);
            }
        }finally {
           // 锁的释放
//            redisLock.unlock();
           lock.unlock();
       }
   }

1.首先判断锁是否存在,

2. 如果存在 则 判断锁的标识是否是自己 ,发现不是则返回,表示锁获取失败

3. 如果判断锁标识是自己的,锁计数+1, 设置锁的有效期, 而后执行业务, 

4. 执行业务时判断锁是否是自己的,如果是则锁计数-1 ,表示退出,而后判断锁计数是否为0

如果为0 则 释放锁,否则重置锁的有效期,执行业务。一旦判断锁不是自己的则马上进行释放锁的操作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值