芝法酱躺平攻略(16)——Java并发控制,抢消费券业务逻辑

一、前言

在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"})

由于本章还没讲到消息队列,故暂时不展示代码实现。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值