一、为何出现库存超卖
库存超卖通常出现在高并发的销售场景中,如团购、秒杀或特价活动期间。这些活动往往会导致访问量激增,成千上万的用户同时抢购有限的商品,这就极易造成库存超卖现象。
具体来说,库存超卖的主要原因包括:
- 并发操作:在高并发场景下,多个用户可能会同时读取到相同的库存数量,并在此基础上进行扣减操作,这可能导致实际销售的商品数量超过库存量。
- 系统延迟:在分布式系统中,由于网络延迟或者数据库锁等待等原因,可能会导致订单处理的延迟,从而在库存扣减之前产生更多的订单请求。
- 缓存不一致性:如果使用缓存来提高性能,缓存数据与数据库数据之间的不一致性也可能导致超卖问题。
- 缺乏适当的并发控制策略:在没有适当并发控制的情况下,多个请求几乎同时到达服务器,对库存资源进行竞争,可能导致超卖问题。
二、如何解决库存超卖
为了防止库存超卖,可以采取以下措施:
- 使用乐观锁或悲观锁:通过锁定机制确保在更新库存时不会被其他事务干扰。
- 减少系统延迟:优化系统架构和数据库设计,减少处理订单的时间。
- 缓存预加载:在活动开始前预先加载缓存,减少对数据库的直接访问。
- 限流和排队:对请求进行限流,确保系统能够平稳处理请求,避免因瞬间流量过大而导致的问题。
- 分布式锁:在分布式环境下使用分布式锁,确保同一时间只有一个节点能够执行特定的代码块。
总的来说,库存超卖是一个复杂的问题,需要综合考虑系统的并发处理能力、数据库的设计以及缓存策略等多个方面,以确保在高并发的销售活动中能够准确控制库存,防止超卖现象的发生
三、使用Redisson分布式锁解决库存超卖
导入 Redisson 和 AOP 的依赖
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson-spring-boot-starter</artifactId> <version>3.24.3</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
新建redisson.yml配置文件,配置以下内容
singleServerConfig: address: "redis://43.139.81.26:6379" database: 9 password: null idleConnectionTimeout: 10000 connectTimeout: 10000 timeout: 3000 retryAttempts: 3 retryInterval: 1500 subscriptionsPerConnection: 5 clientName: null subscriptionConnectionMinimumIdleSize: 1 subscriptionConnectionPoolSize: 50 connectionMinimumIdleSize: 24 connectionPoolSize: 64 dnsMonitoringInterval: 5000 threads: 16 nettyThreads: 32 codec: !<org.redisson.codec.Kryo5Codec> { } transportMode: "NIO"
application.yml文件配置:
spring: redis: redisson: file: "classpath:redisson.yml"
在控制层的创建订单的方法中添加 @DistributedLock 注解,注解配置一些参数
package com.qf.cloud.mq.lock.controller;
import com.qf.cloud.mq.lock.distributed.annotation.DistributedLock;
import com.qf.cloud.mq.lock.distributed.enmus.LockType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("lock")
public class DistributedLockController {
private int num = 100;
@PostMapping("/create")
@DistributedLock(value = "order",lockType = LockType.DEFAULT_LOCK,lockName = "update",moduleName = "order",waitTime = 30,leaseTime = 30)
public int createOrder(){
num--;
return num;
}
}
新建AOP类
package com.qf.cloud.mq.lock.distributed.aop;
import com.qf.cloud.mq.lock.distributed.annotation.DistributedLock;
import com.qf.cloud.mq.lock.distributed.enmus.LockType;
import com.qf.cloud.mq.lock.distributed.enmus.ReadWriteLockType;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.api.RLock;
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import java.lang.reflect.Method;
@Component
@Aspect
public class DistributedLockAspect {
/**
* 固定锁的名称前缀
*/
public static final String DEFAULT_PREFIX_LOCK_NAME = "lock:distributed:";
/**
* 常量定义,表示默认的锁名称为"default"
*/
public static final String DEFAULT_LOCK_NAME = "default";
/**
* RedissonClient是一个用于操作Redis数据库的客户端类,
* 它提供了丰富的API来执行各种Redis操作,如连接管理、数据操作、分布式锁等
*/
@Autowired
private RedissonClient redissonClient;
/**
* 注解切入点
*/
@Pointcut("@annotation(com.qf.cloud.mq.lock.distributed.annotation.DistributedLock)")
public void pointCut() {
}
/**
* Around 环绕注解 (切入点)
*
* @param pjp Spring AOP中的一个接口,提供了一些方法
* @return 返回核心方法的结果
*/
@Around("pointCut()")
public Object locked(ProceedingJoinPoint pjp) {
Object proceed = null;
// 获取切面中的签名信息,获取目标方法的签名信息,包括方法名、参数类型等
Signature signature = pjp.getSignature();
// 判断切面中的签名信息是否为方法签名
if (signature instanceof MethodSignature) {
// 强转
MethodSignature methodSignature = (MethodSignature) signature;
// 获取切面中的签名信息所对应的方法对象
Method method = methodSignature.getMethod();
// 用于获取方法上的 @DistributedLock 注解对象
DistributedLock distributedLock = method.getAnnotation(DistributedLock.class);
// 获取锁对象,可以是重入锁,公平锁,读写锁
RLock lock = getLock(distributedLock);
/**
* 用于尝试获取锁,如果锁可用(即未被其他线程持有),返回true并获取锁
* 如果锁不可用,则立即返回false,不会等待锁的释放
* 三个参数 1、等待时间的长度,底层通过while循环不断获取锁,超过时间未获取到就报错
* 2、等待时间的单位,比如 30,如果是秒的话,就是30秒后就会释放锁,给其他线程上锁
* 3、等待时间的类型,比如 秒,分,时
*/
try {
boolean isLock = lock.tryLock(distributedLock.waitTime(), distributedLock.leaseTime(), distributedLock.unit());
// 判断锁可用?
if (isLock) {
// 继续执行核心代码
proceed = pjp.proceed();
}
} catch (Throwable e) {
throw new RuntimeException(e);
} finally {
// 释放锁
lock.unlock();
}
}
// 返回核心方法的结果
return proceed;
}
/**
* 获取锁对象
*
* @param distributedLock 方法上的注解对象
* @return 返回一个锁对象 可以是重入锁,公平锁,读写锁
*/
private RLock getLock(DistributedLock distributedLock) {
RLock rLock = null;
// 获取注解对象里的lockTpe参数
LockType lockType = distributedLock.lockType();
switch (lockType) {
case FAIR_LOCK:
// getFairLock 获取公平锁,参数是锁的完整名称
rLock = redissonClient.getFairLock(getLockName(distributedLock));
break;
case READ_WRITE_LOCK:
// readWriteLock 获取读写锁,参数是锁的完整名称
RReadWriteLock readWriteLock = redissonClient.getReadWriteLock(getLockName(distributedLock));
// 通过方法上的注解里的 获取 ReadWriteLockType 锁的类型
ReadWriteLockType readWriteLockType = distributedLock.readWriteLockType();
// 是读锁就用读锁,写锁就用写锁
rLock = distributedLock.readWriteLockType().equals(ReadWriteLockType.READ_LOCK) ? readWriteLock.readLock() : readWriteLock.writeLock();
break;
default:
// getLock 获取普通锁,参数是锁的完整名称
rLock = redissonClient.getLock(getLockName(distributedLock));
break;
}
return rLock;
}
/**
* 获取锁的完整名称
*
* @param distributedLock 方法上的注解对象
* @return 完整的锁名称
*/
private String getLockName(DistributedLock distributedLock) {
// 判断切入点参数有没有 lockName,没有就用默认的值,有就用有的
String lockName = ObjectUtils.isEmpty(distributedLock.lockName()) ? DEFAULT_LOCK_NAME : distributedLock.lockName();
// 判断跌入点的参数与没有 moduleName,没有就为空,有就用有的
String moduleName = distributedLock.moduleName();
// 固定锁的名称前缀 lock:distributed:
StringBuffer stringBuffer = new StringBuffer(DEFAULT_PREFIX_LOCK_NAME);
// 如果 moduleName 不为空
if (!ObjectUtils.isEmpty(moduleName)) {
// 锁名称前缀 + moduleName + :
stringBuffer.append(moduleName).append(":");
}
// 锁名称前缀 + moduleName + : + lockName
stringBuffer.append(lockName);
// 例如: lock:distributed:moduleName:default
return stringBuffer.toString();
}
}
切入点的注解类
package com.qf.cloud.mq.lock.distributed.annotation;
import com.qf.cloud.mq.lock.distributed.enmus.LockType;
import com.qf.cloud.mq.lock.distributed.enmus.ReadWriteLockType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.concurrent.TimeUnit;
// RetentionPolicy 是一个枚举类型,它定义了三种保留策略:
// SOURCE:注解仅在源代码中保留,编译时会被忽略。
// CLASS:注解在编译时被保留,但在运行时会被忽略。
// RUNTIME:注解在运行时被保留,可以通过反射机制读取
@Retention(RetentionPolicy.RUNTIME)
public @interface DistributedLock {
/**
* 注解切入点里的参数
* 例如 @DistributedLock(value = "order", lockType = LockType.FAIR_LOCK,
* lockName = "create", moduleName = "test", waitTime = 30,
* leaseTime = 10, unit = TimeUnit.SECONDS)
*/
// value 默认值为空
String value() default "";
// waitTime() 等待时间的长度,底层通过while循环不断获取锁,超过时间未获取到就报错
long waitTime() default 100;
// 等待时间的单位,比如 30,如果是秒的话,就是30秒后就会释放锁,给其他线程上锁
long leaseTime() default 30;
// 等待时间的类型,比如 秒,分,时
TimeUnit unit() default TimeUnit.SECONDS;
// 锁的名称 默认值为空
String lockName() default "";
// 模块名称,组成锁的名称
String moduleName() default "";
// lockType()是一个方法,表示返回一个LockType锁的类型,默认使用 DEFAULT_LOCK 作为锁类型
LockType lockType() default LockType.DEFAULT_LOCK;
// readWriteLockType()是一个方法,表示返回一个ReadWriteLockType锁的类型,默认使用 READ_LOCK 作为锁类型
ReadWriteLockType readWriteLockType() default ReadWriteLockType.READ_LOCK;
}
枚举类
package com.qf.cloud.mq.lock.distributed.enmus;
/**
* 枚举项 锁的类型
*/
@SuppressWarnings("ALL")
public enum LockType {
// 默认
DEFAULT_LOCK,
// 公平锁
FAIR_LOCK,
// 读写锁
READ_WRITE_LOCK;
}
package com.qf.cloud.mq.lock.distributed.enmus;
@SuppressWarnings("ALL")
public enum ReadWriteLockType {
// 读锁
READ_LOCK,
// 写锁
WRITE_LOCK;
}