springboot整合redisson实现分布式锁,实现商品下单库存扣减功能

一、使用场景

分布式服务中有些场景(比如定时任务和库存更新)需要考虑并发执行所可能带来的问题。

如果不使用专业的分布式定时工具(如quartz),而是简单地依赖定时器来做定时任务,那么会有并发执行的情况,可能造成某些问题。

对于库存更新这样的场景,我们需要考虑对同一条记录进行更新并记录所可能出现的问题,比如读未提交造成的记录错乱。为了解决这些问题,我们可以引入分布式事务锁。这种锁可以确保在分布式系统中操作的正确性和一致性。

二、分布式锁的实现过程中可能会遇到的问题

1.程序出现异常导致锁一直不释放,造成死锁

2.锁的过期时间到了,但是业务没处理完,导致其他线程拿到锁,也就是分布式锁无法自动续期

为解决以上问题,推荐使用比较完整优秀的分布式锁:

  • 基于Redisson实现分布式锁原理(Redission是一个独立的redis客户端,是与Jedis、Lettuce同级别的存在)

Redisson的加锁机制如下图所示:

springboot整合redisson实现分布式锁,实现商品下单库存扣减功能_第1张图片

锁资源被多个线程获取,成功则执行lua脚本,保存数据到redis数据库。

如果获取失败: 一直通过while循环尝试获取锁(可自定义等待时间,超时后返回失败)。

Redisson提供的分布式锁是支持锁自动续期的,也就是说,锁的过期时间到了,但是业务没处理完,那么redisson会自动给redis中的目标key延长超时时间,这在Redisson中称之为 Watch Dog 看门狗机制。

三、代码实现

1.依赖和配置文件


   org.redisson
   redisson-spring-boot-starter
   3.15.0

在配置文件中加

spring:
  redis:
    redisson:
      file: classpath:redisson.yaml

然后新建一个redisson.yaml文件,也放在resouce目录下

singleServerConfig:
  idleConnectionTimeout: 10000
  connectTimeout: 10000
  timeout: 3000
  retryAttempts: 3
  retryInterval: 1500
  password: 123456
  subscriptionsPerConnection: 5
  clientName: null
  address: "redis://localhost:6379"
  subscriptionConnectionMinimumIdleSize: 1
  subscriptionConnectionPoolSize: 50
  connectionMinimumIdleSize: 32
  connectionPoolSize: 64
  database: 1
  dnsMonitoringInterval: 5000
threads: 0
nettyThreads: 0
codec: ! {}
transportMode: "NIO"

2.Redis工具类

(1)redisson操作类,本案例只用到tryLock()和unLock()两个方法,其他的可以根据业务灵活选用

package com.sync.task.core.redislock;

import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

/**
 * redisson操作类
 */
@Component
public class RedisDistributedLocker {

    @Autowired
    private RedissonClient redissonClient;

    public RLock lock(String lockKey) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.lock();
        return lock;
    }

    public RLock lock(String lockKey, int leaseTime) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.lock(leaseTime, TimeUnit.SECONDS);
        return lock;
    }

    public RLock lock(String lockKey, TimeUnit unit ,int timeout) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.lock(timeout, unit);
        return lock;
    }

    public boolean tryLock(String lockKey, TimeUnit unit, int waitTime, int leaseTime) {
        RLock lock = redissonClient.getLock(lockKey);
        try {
            return lock.tryLock(waitTime, leaseTime, unit);
        } catch (InterruptedException e) {
            return false;
        }
    }

    public boolean tryLock(String lockKey) {
        RLock lock = redissonClient.getLock(lockKey);
        return lock.tryLock();
    }

    public void unlock(String lockKey) {
        RLock lock = redissonClient.getLock(lockKey);
        //查看是自己加的锁吗,是自己的锁再释放
        if (lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }

    public void unlock(RLock lock) {
        lock.unlock();
    }
}

(2)分布式锁工具类

package com.sync.task.core.redislock;

import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.util.concurrent.TimeUnit;

/**
 * redis分布式锁工具类
 */
@Component
public class RedisLockUtils {

    @Autowired
    private RedisDistributedLocker locker;

    private static RedisDistributedLocker distributedLocker;

    @PostConstruct
    private void init() {
        distributedLocker = locker;
    }

    /**
     * 加锁
     * @param lockKey
     * @return
     */
    public static RLock lock(String lockKey) {
        return distributedLocker.lock(lockKey);
    }

    /**
     * 释放锁
     * @param lockKey
     */
    public static void unlock(String lockKey) {
        distributedLocker.unlock(lockKey);
    }

    /**
     * 释放锁
     * @param lock
     */
    public static void unlock(RLock lock) {
        distributedLocker.unlock(lock);
    }

    /**
     * 带超时的锁
     * @param lockKey
     * @param timeout 超时时间   单位:秒
     */
    public static RLock lock(String lockKey, int timeout) {
        return distributedLocker.lock(lockKey, timeout);
    }

    /**
     * 带超时的锁
     * @param lockKey
     * @param unit 时间单位
     * @param timeout 超时时间
     */
    public static RLock lock(String lockKey, int timeout, TimeUnit unit ) {
        return distributedLocker.lock(lockKey, unit, timeout);
    }

    /**
     * 尝试获取锁
     * @param lockKey
     * @param waitTime 最多等待时间
     * @param leaseTime 上锁后自动释放锁时间
     * @return
     */
    public static boolean tryLock(String lockKey, int waitTime, int leaseTime) {
        return distributedLocker.tryLock(lockKey, TimeUnit.SECONDS, waitTime, leaseTime);
    }

    /**
     * 尝试获取锁
     * @param lockKey
     * @param unit 时间单位
     * @param waitTime 最多等待时间
     * @param leaseTime 上锁后自动释放锁时间
     * @return
     */
    public static boolean tryLock(String lockKey, TimeUnit unit, int waitTime, int leaseTime) {
        return distributedLocker.tryLock(lockKey, unit, waitTime, leaseTime);
    }

    /**
     * 尝试获取锁,不设置等待和过期时间,看门狗才能真正生效,自动续期
     * @param lockKey
     * @return
     */
    public static boolean tryLock(String lockKey) {
        return distributedLocker.tryLock(lockKey);
    }
}

3.业务实现类

保存商品信息到Redis在这里就不做赘述了,需要了解的看我另外一篇文章:

https://mp.csdn.net/mp_blog/creation/editor/130848593

以下是保存到Redis的商品信息:

springboot整合redisson实现分布式锁,实现商品下单库存扣减功能_第2张图片

提交商品订单:

    /**
     * 提交订单
     * @param productId 商品id
     * @param count 购买数量
     * @return
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Result submitOrder(String productId, long count) {
        String productKey = PRODUCT + productId; // PRODUCT_N001
        String stockKey = PRODUCT_STOCK + productId; // PRODUCT_STOCK_N001
        String stockLockKey = PRODUCT_STOCK_LOCK +productId; // PRODUCT_STOCK_LOCK_N001

        // 获取库存锁,不会自动续期,最多等待3秒,超过则放弃获取锁,20秒后锁自动释放
//        boolean res = RedisLockUtils.tryLock(stockLockKey, TimeUnit.SECONDS, 3, 20);
        // 获取库存锁,自动续期
        boolean res = RedisLockUtils.tryLock(stockLockKey);
        if (!res) {
            // 超过等待时间,放弃获取锁
            log.info("线程{} 获取锁失败", stockLockKey);
            return ResponseData.error("系统正忙,请稍后再试");
        }
        log.info("线程{} 获取锁成功", stockLockKey);
        try {
            // 查询库存
            Integer stock = (Integer) redisTemplate.opsForValue().get(stockKey);
            // 缓存没有商品库存,查询数据库
            if (stock == null) {
                Product product = productMapper.getById(productId);
                if (Objects.isNull(product)) {
                    return ResponseData.error("订单提交失败,商品已下架");
                }
                redisTemplate.opsForValue().set(productKey, JSON.toJSONString(product));
                // 数据库的库存信息要根据实际业务来获取,如果商品有规格信息,库存应该根据规格来获取
                stock = product.getStack();
                redisTemplate.opsForValue().set(stockKey, stock);

                return ResponseData.error("订单提交失败,库存不足");
            }

            // 检查库存是否足够
            if (stock < count) {
                return ResponseData.error("订单提交失败,库存不足");
            }

            // 更新数据库中的库存数据
            int row = productMapper.updateStock(productId, stock - count);
            if (row > 0) {
                // 更新Redis中缓存的商品库存数据
                redisTemplate.opsForValue().decrement(stockKey, count);
            }
            // 库存扣减完,创建订单
            Order order = Order.builder()
                    .id(IdUtil.simpleUUID()).count(count).productId(productId).status(1)
                    .build();
            String orderId = orderService.createOrder(order);

            return ResponseData.success(orderId, "订单创建成功");

        } catch (OptimisticLockingFailureException e) {
            // 如果出现乐观锁异常,则释放 Redis 锁并返回错误信息
            log.error("更新库存失败,商品 {} 可能已被更新", productId);
        } catch (Exception e) {
            log.error("系统开小差了~");
        } finally {
            RedisLockUtils.unlock(stockLockKey);
            log.info("线程{} 释放锁", stockLockKey);
        }
        return ResponseData.error("系统正忙,请稍后再试");
    }

你可能感兴趣的:(spring,boot,分布式,java,redis)