一、前言
在web开发中,常常会因为并发而产生数据错误。相比逻辑错误,这种错误更难发现,也更难溯源。所以这部分知识更需要认真学习。
1.1 一种常见的场景
比如数据库表中的某个字段的+1操作。业务上,首先从数据库中读取该字段,而后把该字段+1,再回写进数据库。假如该字段值为a,如果两个请求同时到达。我们期望该字段最终结果是a+2。但实际情况可能是,当2个请求同时到达时,servlet分配2个线程,两个线程都读取了字段值a,分别对该字段+1,而后回写到数据库。最终,该字段的值却是a+1。
1.2 常见解决方案
1.2.1 乐观锁
我们在芝法酱躺平攻略(3)中,已经为我们mybatis-plus的操作框架添加了乐观锁。所谓乐观锁,就是在数据库表中添加了一个version字段,当更新时,会在sql上拼接一个条件:
UPDATE xxx SET ...,version = version + 1
where
...
and version = #version
当另一个线程已经更新过,则该线程的更新会失败
这样就有效的保证了更新操作不会造成数据错误,但用户体验很不好,会让部分用户操作失败。不过这种处理方式效率比较高
1.2.2 内存缓存上锁
假定该业务服务只有一个服务器,可以对内存缓存对象上锁。在操作前首先在内存缓存中申请特定key的锁,如果该key已被申请,则阻塞。
可以使用JVM的关键字synchronized,表示锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
当然,更好的做法应该是用reactor模式,当然目前我还不会搞,嘻嘻。
关于Java中synchronized,Lock,ReentrantLock 的异同,可以参考这篇博客
在八股文,Java锁的升级上,可以看马士兵的视频,本攻略强调最佳实践,故在此不再赘述。
这种内存缓存上的上锁,如今微服务的时代,对业务开发基本没有参考价值,毕竟业务服务可能部署在多个微服务上的。那这种基于内存的锁,就没有任何意义。
但Java锁,并发操作依然是个显学,用于开发类似消息队列的中间件,是很有意义的。
但如果是业务开发,如果一些逻辑需要在Component中添加一个字段,该字段会被多个线程共享,这个时候就要考虑Java的那些并发知识点。但在互联网上搞八股文的孔乙己们,真的在实际开发中使用过这种模式开发么,如果这种考察还广泛的出现在面试中,这种现象很值得我们思考。
1.2.3 悲观锁-mysql行锁
在事务中,把事务注解加上如下配置
@Transactional(isolation = Isolation.READ_COMMITTED)
在查找时,在select后面加上for update
begin;
select key_int_a from test_table for update;
// java 获取该值,并在内存+1,赋值为intA
update test_table set key_int_a = #intA
commit;
用这种方式的话,线程A进入了查找,当线程B也需要查找时,如果线程A的事务没有进行完,线程B的查找请求会被阻塞起来。
这种方式实现了1.1提到的问题的并发控制,但限制却很明显,这只能是行锁。
但有些数据库表是这样存的,比如用户的消费券,主键,用户Id,消费券Id。在抢优惠券时,需要对和用户Id相关的数据上锁,不允许其他线程插入和这个用户Id相关的数据。这样用mysql就不好实现了
1.2.4 基于redis的redisson
那么,我们可以把思路退回到1.2.2,如果我们不把锁的字段放在内存中,把控制并发的字段放在redis中,这不就解决了么。
redis还支持lua脚本,并对该脚本的执行保证原子性。
在实际开发中,会开发一个RedissonLock的注解,让代码变得更优化,这将在后面介绍。
二、业务准备
为了让大家对该问题有更好的感悟,我打算构想一个虚构的电商平台抢购物券的需求。
2.1 需求概述
商家可以创建消费券。
消费券有库存概念,创建的消费券全局只能分发storage的消费券,每个用户只能抢userGetMax个同类型的消费券。
2.2 数据库设计
数据库实体
CouponEntity
@Data
@TableName("busy_coupon")
@Schema(title = "CouponEntity对象", description = "消费券表")
public class CouponEntity extends BaseEntity {
@Schema(title = "消费券名")
private String name;
@Schema(title = "生效条件类型")
private ECouponTriggerType effectType;
@Schema(title = "生效条件值")
private Double effectVal;
@Schema(title = "优惠类型")
private ECouponTriggerType bargainType;
@Schema(title = "优惠值")
private Double bargainVal;
@Schema(title = "库存")
private Integer storage;
@Schema(title = "用户最大领取数")
private Integer userGetMax;
@Schema(title = "公司Id")
private Long companyAuthId;
@Schema(title = "超时日期")
private LocalDate expireDate;
}
CouponObjectEntity
@Data
@TableName("busy_coupon_object")
@Schema(title = "CouponObjectEntity对象", description = "消费券实体")
public class CouponObjectEntity extends SysBaseEntity {
@Schema(title = "用户Id")
private Long userId;
@Schema(title = "消费券Id")
private Long couponId;
@Schema(title = "是否已使用")
private Boolean used;
@Schema(title = "公司权限Id")
private Long companyAuthId;
@Schema(title = "超时日期")
private LocalDate expireDate;
}
CompanyEntity
@Data
@TableName("sys_company")
@Schema(title = "公司对象", description = "公司表")
public class CompanyEntity extends BaseEntity {
@Schema(title = "鉴权节点")
private Long authId;
@Schema(title = "公司名称")
private String name;
@Schema(title = "公司全称")
private String nameWhole;
@Schema(title = "公司介绍")
private String description;
}
2.3 接口
@Api(tags = "CouponApi-消费券接口")
@RequestMapping("/api/coupon")
@Slf4j
@ZfRestController
@RequiredArgsConstructor
public class CouponApi {
private final ICouponService mCouponService;
private final ICouponObjectService mCouponObjectService;
private final ITokenUtil mTokenUtil;
@RequireRole(roles = "company")
@Operation(summary = "消费券-创建")
@PostMapping(value = "")
CouponEntity createCoupon(@Parameter(description = "消费券配置") @RequestBody CouponDto pCouponDto){
CouponEntity couponEntity = mCouponService.create(pCouponDto);
return couponEntity;
}
@RequireRole(roles = "company")
@Operation(summary = "消费券-更新")
@PutMapping(value = "/{id}")
CouponEntity editCoupon(
@Parameter(description = "id") @PathVariable(name = "id") Long id,
@Parameter(description = "消费券配置") @RequestBody CouponDto pCouponDto){
CouponEntity couponEntity = mCouponService.edit(id,pCouponDto);
return couponEntity;
}
@Operation(summary = "消费券-分页")
@GetMapping(value = "/page")
Page<CouponEntity> pageCoupon(
@Parameter(description = "当前页") @RequestParam(name = "current") Integer pCurrent,
@Parameter(description = "页大小") @RequestParam(name = "size") Integer pSize,
@Parameter(description = "公司权限Id") @RequestParam(name = "companyId",required = false) Long pCompanyAuthId,
@Parameter(description = "消费券名称") @RequestParam(name = "name",required = false) String pName){
Page<CouponEntity> couponEntityPage = mCouponService.page(pCurrent,pSize,pCompanyAuthId,pName);
return couponEntityPage;
}
@RequireRole(roles = "guest")
@Operation(summary = "消费券-获取")
@GetMapping(value = "/{id}/seize")
CouponObjectEntity seizeCoupon(
@Parameter(description = "消费券Id") @PathVariable(name = "id") Long pCouponId) throws InterruptedException {
TokenObject tokenObject = mTokenUtil.getTokenObject();
CouponObjectEntity couponObjectEntity = mCouponObjectService.seizeCoupon(tokenObject.getId(),pCouponId);
return couponObjectEntity;
}
@Operation(summary = "消费券实体-分页")
@GetMapping(value = "/object/page")
Page<CouponObjectEntity> listCouponObject(
@Parameter(description = "当前页") @RequestParam(name = "current") Integer pCurrent,
@Parameter(description = "页大小") @RequestParam(name = "size") Integer pSize){
TokenObject tokenObject = mTokenUtil.getTokenObject();
Page<CouponObjectEntity> couponObjectEntityPage = mCouponObjectService.page(pCurrent,pSize,tokenObject.getId());
return couponObjectEntityPage;
}
}
2.4 未加锁的实现
为所见篇幅,接口就不再展示
CouponServiceImpl
@RequiredArgsConstructor
@Service
public class CouponServiceImpl implements ICouponService {
private final ICouponDbService mCouponDbService;
private final ITokenUtil mTokenUtil;
@Transactional(rollbackFor = Exception.class)
@Override
public CouponEntity create(CouponDto pCouponDto) {
CouponEntity couponEntity = DbDtoEntityUtil.createFromDto(pCouponDto,CouponEntity.class);
TokenObject tokenObject = mTokenUtil.getTokenObject();
Map<String, TokenAuthNodeDto> authNodeDtoMap = tokenObject.getAuthNodeInfo();
TokenAuthNodeDto tokenAuthNodeDto = authNodeDtoMap.get(AuthConst.DOMAIN_COMPANY);
couponEntity.setCompanyAuthId(tokenAuthNodeDto.getAuthId());
mCouponDbService.savePull(couponEntity.getId(),couponEntity);
return couponEntity;
}
@Override
public CouponEntity edit(Long pId, CouponDto pCouponDto) {
CouponEntity orgCouponEntity = mCouponDbService.check(pId);
CouponEntity couponEntity = DbDtoEntityUtil.editByDto(orgCouponEntity,pCouponDto,CouponEntity.class);
mCouponDbService.updatePull(pId,couponEntity);
return couponEntity;
}
@Override
public Page<CouponEntity> page(Integer pCurrent, Integer pSize, Long pCompanyAuthId, String pName) {
Page<CouponEntity> pageCfg = new Page<>(pCurrent,pSize);
LambdaQueryWrapper<CouponEntity> queryWrapper = Wrappers.<CouponEntity>lambdaQuery()
.gt(CouponEntity::getStorage,0)
.eq(null != pCompanyAuthId,CouponEntity::getCompanyAuthId,pCompanyAuthId)
.like(StringUtils.hasText(pName),CouponEntity::getName,pName);
return mCouponDbService.page(pageCfg,queryWrapper);
}
}
CouponObjectServiceImpl
@Slf4j
@RequiredArgsConstructor
@Service
public class CouponObjectServiceImpl implements ICouponObjectService {
private final ICouponDbService mCouponDbService;
private final ICouponObjectDbService mCouponObjectDbService;
private final ITokenUtil mITokenUtil;
@Transactional(rollbackFor = Exception.class)
@Override
public CouponObjectEntity seizeCoupon(Long pUserId, Long pCouponId) throws InterruptedException {
CouponEntity couponEntity = mCouponDbService.check(pCouponId);
LocalDate expire = couponEntity.getExpireDate();
if(LocalDate.now().isAfter(expire)){
throw new ServiceException("该消费券已过期");
}
TokenObject tokenObject = mITokenUtil.getTokenObject();
// check storage
int storage = couponEntity.getStorage();
log.info("用户"+tokenObject.getNickName()+"抢优惠券"+couponEntity.getName()+"_"+couponEntity.getId()+" 此时库存为"+storage);
if(storage <= 0){
throw new ServiceException("无库存");
}
// check available
LambdaQueryWrapper<CouponObjectEntity> couponObjectQueryWrapper = Wrappers.<CouponObjectEntity>lambdaQuery()
.eq(CouponObjectEntity::getCouponId,pCouponId)
.eq(CouponObjectEntity::getUserId,tokenObject.getId());
long cnt = mCouponObjectDbService.count(couponObjectQueryWrapper);
int userGetMax = couponEntity.getUserGetMax();
log.info("用户已拥有"+cnt+"个该优惠券");
if(userGetMax >0 && cnt >= couponEntity.getUserGetMax()){
throw new ServiceException("领取上限");
}
// 插入消费券实体
CouponObjectEntity couponObjectEntity = new CouponObjectEntity();
couponObjectEntity.createInit();
couponObjectEntity.setCouponId(pCouponId);
couponObjectEntity.setUserId(tokenObject.getId());
couponObjectEntity.setUsed(false);
couponObjectEntity.setCompanyAuthId(couponEntity.getCompanyAuthId());
couponObjectEntity.setCouponId(pCouponId);
couponObjectEntity.setExpireDate(couponEntity.getExpireDate());
// 更新库存
couponEntity.setStorage(storage-1);
Thread.sleep(1000);
mCouponDbService.updatePull(couponEntity.getId(),couponEntity);
// 插入消费券实体
Thread.sleep(3000);
mCouponObjectDbService.save(couponObjectEntity);
return couponObjectEntity;
}
@Override
public Page<CouponObjectEntity> page(Integer pCurrent, Integer pSize, Long pUserId) {
Page<CouponObjectEntity> couponObjectEntityCfg = new Page<>(pCurrent,pSize);
LocalDate today = LocalDate.now();
LambdaQueryWrapper<CouponObjectEntity> queryWrapper = Wrappers.<CouponObjectEntity>lambdaQuery()
.eq(CouponObjectEntity::getUserId,pUserId)
.ge(CouponObjectEntity::getExpireDate,today);
return mCouponObjectDbService.page(couponObjectEntityCfg,queryWrapper);
}
}
2.5 pom引用以及配置
2.5.1 pom配置
<dependencies>
<dependency>
<groupId>indi.zhifa.recipe.bailan5</groupId>
<artifactId>auth-client-default</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
2.5.2 auth-client-default的接入
pom引用
<dependencies>
<dependency>
<groupId>indi.zhifa.recipe</groupId>
<artifactId>framework-auth-client-default</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>indi.zhifa.recipe</groupId>
<artifactId>framework-enums-client</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
添加数据库的handler
@Component
public class MetaObjectHandler extends DefaultMetaObjectHandler {
public MetaObjectHandler(IBaseTokenUtil pTokenUtil){
super(pTokenUtil);
}
@Override
protected void otherInsertFill(MetaObject pMetaObject) {
}
@Override
protected void otherUpdateFill(MetaObject pMetaObject) {
}
}
2.6 用户服务器
本篇的鉴权基于 芝法酱躺平攻略-9、芝法酱躺平攻略-10、芝法酱躺平攻略-11
2.6.1 pom引用
<dependencies>
<dependency>
<groupId>indi.zhifa.recipe</groupId>
<artifactId>framework-auth-user-default</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>indi.zhifa.recipe.bailan5</groupId>
<artifactId>auth-client-default</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>indi.zhifa.recipe</groupId>
<artifactId>framework-enums-client</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
2.6.2 初始化程序
@Slf4j
@Component
public class AuthNodeServiceImpl extends BaseAuthNodeServiceImpl implements IAuthNodeService {
private final IAuthNodeDbService mDepartmentDbService;
private final ICompanyDbService mCompanyDbService;
public AuthNodeServiceImpl(IBaseAuthNodeRelevantDao pDepartmentRelevantDao,
IAuthNodeDbService mDepartmentDbService,
ICompanyDbService pCompanyDbService){
super(pDepartmentRelevantDao);
this.mDepartmentDbService = mDepartmentDbService;
mCompanyDbService = pCompanyDbService;
}
@Override
protected void onInit(String pDomain, List<AuthNodeCfg> pAuthNodeCfgList, List<BaseAuthNodeEntity> pSavingBaseAuthNodeEntityList) {
assert(pAuthNodeCfgList.size() == pSavingBaseAuthNodeEntityList.size());
List<CompanyEntity> companyEntityList = new ArrayList<>();
for(int i=0;i<pAuthNodeCfgList.size();i++){
AuthNodeCfg authNodeCfg = pAuthNodeCfgList.get(i);
if("rt".equals(authNodeCfg.getCode())){
continue;
}
BaseAuthNodeEntity baseAuthNodeEntity = pSavingBaseAuthNodeEntityList.get(i);
JSONObject jsonObject = authNodeCfg.getCfg();
switch (pDomain){
case "zf-company":// 卖家
CompanyEntity companyEntity = DbDtoEntityUtil.createFromDto(jsonObject,CompanyEntity.class);
companyEntity.setAuthId(baseAuthNodeEntity.getId());
companyEntityList.add(companyEntity);
break;
}
}
mCompanyDbService.saveBatch(companyEntityList);
}
@Override
protected void onDepCodeChange(BaseAuthNodeEntity pBaseAuthNodeEntity) {
}
@Override
protected void onDepDelete(BaseAuthNodeEntity pBaseAuthNodeEntity) {
mCompanyDbService.deleteById(pBaseAuthNodeEntity.getId());
}
@Override
public AuthNodeEntity create(Long pParentId, AuthNodeCreateDto pAuthNodeCreateDto) {
AuthNodeEntity authNodeEntity = DbDtoEntityUtil.createFromDto(pAuthNodeCreateDto, AuthNodeEntity.class);
return (AuthNodeEntity)super.create(pParentId, authNodeEntity);
}
@Override
public AuthNodeEntity edit(Long pId, AuthNodeEditDto pAuthNodeEditDto) {
AuthNodeEntity orgAuthNodeEntity = mDepartmentDbService.check(pId);
AuthNodeEntity newAuthNodeEntity = DbDtoEntityUtil.editByDto(orgAuthNodeEntity,pAuthNodeEditDto, AuthNodeEntity.class);
newAuthNodeEntity = mDepartmentDbService.updatePull(newAuthNodeEntity.getId(), newAuthNodeEntity);
return newAuthNodeEntity;
}
}
2.6.3 初始化配置
初始框架-1级部门
其余配置和原先的差不多,就不赘述
2.6.4 测试
在用户服务登陆,创建一个消费券
{
"bargainType": "TOTAL_PRICE",
"bargainVal": 30,
"effectType": "TOTAL_PRICE",
"effectVal": 300,
"name": "测试消费券b",
"storage": 5,
"userGetMax": 2,
"expireDate": "2023-05-30"
}
然后分别用买家A,买家B登陆,获取token
浏览器中打开6个链接,键入如下,并不按回车:
localhost:8081/api/coupon/消费券id/seize?auth=token
3个买家A,3个买家B
准备好后,快速的挨个按回车,观察现象。
可以看到会出现数据库存储失败之类的提示,也就是触发了我们的乐观锁
三、redisson的实现
redisson是我们推荐的一种可重入分布式锁,相当于把内存锁移到了redis中。
3.1 redisson的自定义注解
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RedissonLock {
/**
* 前缀
*/
String prefix() default "lock:";
/**
* id的名称
*/
String key() default "#pId";
/**
* 上锁最大等待时间
*/
long waitTime() default -1L;
/**
* 线程持续时间
*/
long leaseTime() default 10L;
}
3.2 redisson的AOP
@AllArgsConstructor
@Aspect
@Slf4j
@Component
public class RedissonHandler {
private RedissonClient mRedissonClient;
@Around("@annotation(pRedissonConfig)")
public Object around(ProceedingJoinPoint joinPoint, RedissonLock pRedissonConfig) throws Throwable {
String prefix = pRedissonConfig.prefix();
String id = doKey(joinPoint,pRedissonConfig.key());
String key = prefix+id;
RLock rLock = mRedissonClient.getLock(prefix+id);
if(pRedissonConfig.leaseTime() > 0){
// 不加看门狗
try{
if(pRedissonConfig.waitTime() > 0L){
boolean ret = rLock.tryLock(pRedissonConfig.waitTime(), pRedissonConfig.leaseTime(), TimeUnit.SECONDS);
if(!ret){
throw new ServiceException(key+" 上锁失败,请联系管理员");
}
}else{
rLock.lock(pRedissonConfig.leaseTime(),TimeUnit.SECONDS);
}
}catch (InterruptedException interruptedException){
throw new ServiceException(key+" 上锁失败,失败信息是 "+interruptedException+"请联系管理员");
}
}else{
// 加看门狗
if(pRedissonConfig.waitTime() > 0L){
boolean ret = rLock.tryLock(pRedissonConfig.waitTime(), TimeUnit.SECONDS);
if(!ret){
throw new ServiceException(key+" 上锁失败,请联系管理员");
}
}else{
rLock.lock();
}
}
Object ret = null;
try{
ret = joinPoint.proceed(joinPoint.getArgs());
}catch (Exception ex){
throw ex;
}finally {
try{
rLock.unlock();
}catch (Exception ex){
log.error("解锁超时");
}
}
return ret;
}
public String doKey(ProceedingJoinPoint joinPoint, String key) {
//获取方法的参数名和参数值
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
List<String> paramNameList = Arrays.asList(methodSignature.getParameterNames());
List<Object> paramList = Arrays.asList(joinPoint.getArgs());
//将方法的参数名和参数值一一对应的放入上下文中
EvaluationContext ctx = new StandardEvaluationContext();
for (int i = 0; i < paramNameList.size(); i++) {
ctx.setVariable(paramNameList.get(i), paramList.get(i));
}
SpelExpressionParser spelExpressionParser = new SpelExpressionParser();
// 解析SpEL表达式获取结果
String value = spelExpressionParser.parseExpression(key).getValue(ctx).toString();
return value;
}
}
3.3 redisson的AOP
@AllArgsConstructor
@Aspect
@Slf4j
@Component
public class RedissonHandler {
private RedissonClient mRedissonClient;
@Around("@annotation(pRedissonConfig)")
public Object around(ProceedingJoinPoint joinPoint, RedissonLock pRedissonConfig) throws Throwable {
String prefix = pRedissonConfig.prefix();
List<String> keys = new ArrayList<>();
for(String keyExp : pRedissonConfig.keys()){
String key = prefix+doKey(joinPoint,keyExp);
keys.add(key);
}
List<RLock> rLocks = new ArrayList<>();
for(String key : keys){
RLock rLock = mRedissonClient.getLock(key);
rLocks.add(rLock);
}
for(int i=0;i<keys.size();i++){
RLock rLock = rLocks.get(i);
String key = keys.get(i);
if(pRedissonConfig.leaseTime() > 0){
// 不加看门狗
try{
if(pRedissonConfig.waitTime() > 0L){
boolean ret = rLock.tryLock(pRedissonConfig.waitTime(), pRedissonConfig.leaseTime(), TimeUnit.SECONDS);
if(!ret){
throw new ServiceException(key+" 上锁失败,请联系管理员");
}
}else{
rLock.lock(pRedissonConfig.leaseTime(),TimeUnit.SECONDS);
}
}catch (InterruptedException interruptedException){
throw new ServiceException(key+" 上锁失败,失败信息是 "+interruptedException+"请联系管理员");
}
}else{
// 加看门狗
if(pRedissonConfig.waitTime() > 0L){
boolean ret = rLock.tryLock(pRedissonConfig.waitTime(), TimeUnit.SECONDS);
if(!ret){
throw new ServiceException(key+" 上锁失败,请联系管理员");
}
}else{
rLock.lock();
}
}
}
Object ret = null;
try{
ret = joinPoint.proceed(joinPoint.getArgs());
}catch (Exception ex){
throw ex;
}finally {
try{
for(RLock rLock : rLocks){
rLock.unlock();
}
}catch (Exception ex){
log.error("解锁超时");
}
}
return ret;
}
public String doKey(ProceedingJoinPoint joinPoint, String key) {
//获取方法的参数名和参数值
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
List<String> paramNameList = Arrays.asList(methodSignature.getParameterNames());
List<Object> paramList = Arrays.asList(joinPoint.getArgs());
//将方法的参数名和参数值一一对应的放入上下文中
EvaluationContext ctx = new StandardEvaluationContext();
for (int i = 0; i < paramNameList.size(); i++) {
ctx.setVariable(paramNameList.get(i), paramList.get(i));
}
SpelExpressionParser spelExpressionParser = new SpelExpressionParser();
// 解析SpEL表达式获取结果
String value = spelExpressionParser.parseExpression(key).getValue(ctx).toString();
return value;
}
}
3.4 给seizeCoupon加上RedissonLock
@RedissonLock(keys = {"#pCouponId"})
思考,这样做是否会造成效率上的问题,有没有更好的解决方案。
很显然,这样地做法会让所有该优惠券的争抢变成一个线性,每个操作需要伴随大量的sql操作,当成千上万个用户一起争抢该消费券时,就会造成严重的卡顿。
我们能否换一个思路呢,对于消费券库存,我们存在redis里,利用redis的单线程机制,我们可以确保该库存的扣减不会造成并发问题。对于每次扣减,我们再通过消息队列的方式,让另一个线程去回写结果。
在上锁上,我们只对用户Id+pCouponId上锁
@RedissonLock(keys = {"#pUserId+'-'+#pCouponId"})
由于本章还没讲到消息队列,故暂时不展示代码实现。