java多线程:redis分布式锁在不同业务场景下的应用

实际开发过程中遇到了利用redis分布式锁解决评价并发提交问题,以及下单的并发提交问题,本文主要注重redis分布式锁在不同业务场景中的应用,以后再慢慢深入redis分布式锁的原理;

1.应用场景1:唯一订单评价的并发提交问题;

在评价中心建设过程中,由于网络原因,用户多次点击了评价提交按钮,前端没有限制,因此大量的请求发送到后台,造成同一订单写入多条同样的评价记录;

原因分析:接口幂等没有做好,一般来说,用户对于同一操作发起的一次请求或者多次请求的结果应该是一致的,数据库不应该写入重复数据。

解决方案:为了保证接口的幂等,考虑利用redis分布式锁来锁住评价提交请求,保证任意时刻只有一个请求执行写入操作(互斥锁),同时对于订单评价提交问题,我们只需要锁住订单号即可,对于重复请求没有获取到锁可以直接返回,可以利用分布式锁的非阻塞特性;

利用分布式锁进行接口幂等的一般流程如下:

上述加锁本质上是一种非阻塞锁,其他线程没拿到锁就会直接返回。如果出现最先拿到锁的请求执行完毕解锁,此时还有重复请求过来,我们好可以通过查库判断已评价来控制接口的幂等;

为了方便理解,本文给出该问题的交互时序图:

/**

  • 评价提交的并发校验,不需手动释放锁
    */
    public void rateSubmitConcurrentLock(String settingsId,String carrierId){

boolean lockResult = true;
// 加锁key: prefix + settingsId + carrierId
StringBuilder key = new StringBuilder();
key.append(CommonConstants.COMMENT_CREATE_CONCURRENT_LOCK).append(settingsId)
.append(“_”).append(carrierId);
try {

RedisLock lock = RedisClientManagement.createLock(key.toString());
//判断是否获得锁,并设置锁过期时间(非阻塞)
lockResult = lock.acquireLock(LOCK_EXPIRE_TIME);

} catch (Exception e) {
log.error(“获取redis锁异常”);
LogMetricUtils.logErrorMetric(“获取redis锁异常”);
}
// 未获得锁的线程直接抛异常
if (!lockResult){
throw new RateBizException(RateErrors.CONCURRENT_LOCK_ERROR);
}
}

只设置线程同步执行的超时时间(3s),如果线程A拿到锁之后,在3s内执行了评价提交,但是没有执行到token更新(或者卡在token更新),然后自动解锁之后,线程C拿到锁,token没有更新,又一次执行了评价提交;

对于这种问题,可以通过手动释放锁的方式,更细粒度的使用锁,确保评价提交和token更新都在锁内;

/**

  • 评价提交的并发校验,更细粒度的写法,手动释放锁;
    */
    public RedisLock submitConcurrentLock(String settingsId,String carrierId){

boolean lockResult = true;
RedisLock lock = null;
// 加锁key: prefix + settingsId + carrierId
StringBuilder key = new StringBuilder();
key.append(CommonConstants.COMMENT_CREATE_CONCURRENT_LOCK).append(settingsId)
.append(“_”).append(carrierId);
try {

lock = RedisClientManagement.createLock(key.toString());
//判断是否获得锁,并设置锁过期时间(非阻塞)
lockResult = lock.acquireLock(LOCK_EXPIRE_TIME);

} catch (Exception e) {
log.error(“获取redis锁异常”);
LogMetricUtils.logErrorMetric(“获取redis锁异常”);
}
// 未获得锁的线程直接抛异常
if (!lockResult){
throw new RateBizException(RateErrors.CONCURRENT_LOCK_ERROR);
}
return lock;
}

评价提交引入redis分布式锁的逻辑代码:

// 加锁
RedisLock lock = concurrentChecker.submitConcurrentLock(reqDTO.getSettingsId(),reqDTO.getCarrierId());
CommentCreateResDTO resDTO = null;

try {

// 评价提交业务,逻辑代码
// 获取评价token 对象,并对token校验,判断订单是否已经评价
String userId = String.valueOf(reqDTO.getUserNewId());
String rateToken = reqDTO.getRateToken();
RateTokenPack tokenPack = rateTokenManager.getAndRenewToken(userId, Long.valueOf(reqDTO.getSettingsId()), reqDTO.getCarrierId(), rateToken);
if (!tokenPack.getHasToken()) {
throw new RateBizException(RateErrors.USER_TOKEN_ERROR);
}
RateToken token = tokenPack.getToken();
if (Boolean.TRUE.equals(token.getExistRate())) {
throw new RateBizException(RateErrors.DUPLICATE_COMMENT_ERROR);
}

// 评价提交逻辑,省略部分对象转换逻辑
Result res = commentOperationService.createComment(request);
resDTO = res.getData();

// todo 重置redis评价曝光限制和城市回收限制
refreshCache(token);

// 评价已经创建,重新保存已评价token
token.setExistRate(true);
token.setExistKey(new RateKey(resDTO.getCommentId(), resDTO.getRateTime()));
rateTokenManager.saveToken(token, rateToken);

} catch (RateBizException e) {
throw e;
} finally {
//释放锁
lock.releaseLock();

}

2.应用场景2:大转盘抽奖问题(抽奖点击并发导致剩余次数为-1);

https://blog.csdn.net/weixin_40898368/article/details/96993944

剩余次数是 remainTimes = setTimes - drawTimes,即是由设置的活动抽奖次数减去已经抽的次数,设置的抽奖次数是在活动信息表里面,已经抽奖次数是count中奖记录表中中奖条数。首先,一个抽奖活动一个用户可以抽多次,-1问题出现的原因是因为这个用户疯狂的点击抽奖,前端没有限制,因此大量的请求发送到后台,当用户把抽奖次数耗光了,只剩下1次抽奖机会,两个请求同时过来,验证的时候发现剩余抽奖次数都是1,因此都可以进行抽奖,所以抽了两回,记录插入两回,剩余次数变成了1-2=-1

     //加锁
    String requestId = UUID.randomUUID().toString();
    String lockKey = RedisKeyConstant.IDK_LUCKY_DRAW_KEY + customerId;
    RedisLock lock = RedisClientManagement.createLock(lockKey.toString());
    if(!lock.acquireLock(LOCK_EXPIRE_TIME)){   
        //抛出异常“您的操作过于频繁,请稍后再试”
        throw new LuckyDrawException(ErrorCode.LUCKY_DRAW_TOO_MANY_TIMES);
    }

    try{
        //业务代码开始
        //得到剩余次数
        int remainTimes = getRemainTimes(luckyDrawDoc,customerId);
        //进行抽奖权限验证
        //抽奖
        //发奖
        //插入抽奖记录
    }catch (Exception e){
        throw e;
    }finally {
        //释放锁
        lock.releaseLock();
    }

3.应用场景3:评价配置读取控制并发访问;

在评价中心建设过程中,经常需要通过评价配置id来查询评价配置,利用本地缓存来存储热点评价配置信息,首先spring容器启动的时候会加载配置信息到缓存;每次调用获取配置的接口都会先从本地缓存获取,本地缓存失效才会走数据库查询;

redis锁在这里的主要应用就是,针对缓存失效的情况,所有的评价配置获取请求都会走查询数据库,大量配置查询请求有可能打挂数据库,因此需要对评价配置请求进行并发控制;

加锁解决方案:为了保证所有的评价配置获取请求不会都走数据库查询,利用入参settingsId进行加锁,而且每一个请求都应当有返回结果,多个线程请求进行排队处理,因此这里应当使用阻塞锁;(阻塞锁确保每一个走数据库查询的请求都得到查询结果)

本文给出该问题并发线程加锁的交互时序图:

下面是走数据库查询的配置详情业务逻辑:
/**

  • 根据settingsId获取配置详情,本地缓存失效会走
  • @param settingsId
  • @return
    */
    public SettingsExtVo getSettingsById(Long settingsId) {
    return settingsDetailCaches.get(settingsId, id -> loadSettings(settingsId));
    }

/**

  • 加载配置详情

  • @param settingsId

  • @return
    */
    private SettingsExtVo loadSettings(Long settingsId) {
    List settingsExtVos = null;
    RedisLock redisLock = null;
    try {
    //对settingsId加分布式锁
    redisLock = concurrentChecker.settingsCacheLock(settingsId);
    if (null != redisLock) {
    // 加锁成功,判断缓存中是否有值
    SettingsExtVo present = settingsDetailCaches.getIfPresent(settingsId);
    if (null != present) {
    return present;
    }

    log.info(“loading cache from db:{}”, settingsId);
    SettingsVO settingsVO = new SettingsVO().queryBySettingsId(settingsId);
    settingsExtVos = loadSettingDetails(Lists.newArrayList(settingsVO));
    }

} catch (Exception e) {
log.error(“settings cache lock error:{},{}”, settingsId, e);
} finally {
if (redisLock != null) {
redisLock.releaseLock();
}
}
return CollectionUtils.isEmpty(settingsExtVos)? null : settingsExtVos.get(0);
}

/**

  • 评价配置查询加锁(查库)
  • @param settingsId
  • @return
    */
    public RedisLock settingsCacheLock(Long settingsId) {

RedisLock lock = null;
StringBuilder key = new StringBuilder();
key.append(CommonConstants.COMMENT_CACHE_CURRENT_LOCK).append(settingsId);
lock = RedisClientManagement.createLock(key.toString());

if (!lock.blockAcquireLock(2000, 2500)) { // 加锁超时时间,阻塞等待时间
log.error(“settings cache lock error:{}”, settingsId);
}

return lock;
}

4.应用场景4:充电宝应用扫机柜创建预置订单的控制;

背景/原因分析:共享充电宝业务中,在创建预置订单过程中,会产生并发场景;

  • 场景一:同一用户点击前端借出充电宝多次,产生多个创建预置订单的请求;
  • 场景二:多个人同时扫同一个机柜,此时会出现争抢同一个充电宝的情况,而且机柜的并发弹出能力有限;

解决方案:

下面是充电宝服务中,关于预置订单创建的加锁业务逻辑;

//分布式锁
RedisLock lock = RedisLockUtil.lock(CommonConstants.CREATE_LOCK_PRE+orderChargersCreateReqDTO.getCabinetId(),CommonConstants.LOCK_TIME,CommonConstants.LOCK_TIME_OUT,Errors.CREATE_LOCK_ERROR);
ChargersOrderEntity orderEntity = null;
try{
// 从机柜查询可用充电宝,可用且未被锁定;

// 利用数据库唯一索引吗,对充电宝加锁;
boolean addLockRes =  chargerStatusInnerService.chargersLockInnerService.addLock(orderChargersCreateReqDTO.getUserNewId(),
        chargerStatusResDTO.getChargerId(),orderChargersCreateReqDTO.getCabinetId());
if (!addLockRes){
    throw new ChargersAppException(Errors.CHARGERS_LOCK_FAILED);
}
orderEntity = transactionTemplate.execute(status -> {
   // 创建交易单
   // 超时中心发送超时关单消息
   // 创建冻结凭证号
   // 创建业务单
});

}catch (Exception e){
log.error(“订单创建失败:{}”,e.toString());
throw new ChargersAppException(Errors.CREATE_ERROR);
}finally {
lock.releaseLock();
}

/**

  • 加锁通用代码

  • @param key

  • @param lockTime

  • @param error

  • @return
    */
    public static RedisLock lock(String key, long lockTime, long timeout, Errors error) {
    boolean lockResult = true;
    RedisLock lock = null;
    try {
    lock = RedisClientManagement.createLock(key);
    lockResult = lock.blockAcquireLock(lockTime, timeout);
    } catch (Exception e) {
    log.error(“获取reids锁异常”, e);
    LogMetricUtils.logErrorMetric(“获取reids锁异常”);
    }

    if (!lockResult) {
    throw new ChargersAppException(error);
    }

    return lock;
    }

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
回答: Redis分布式锁适用于多个应用实例之间需要协调互斥访问同一个资源的场景。它可以帮助我们控制分布式系统中对共享资源的访问,避免并发问题和重复操作。常见的使用场景包括: 1. 分布式任务调度:多个应用实例需要定时执行某个任务,通过使用分布式锁可以确保只有一个实例执行任务,避免重复执行。 2. 分布式缓存更新:多个应用实例需要同时更新某个缓存数据,通过使用分布式锁可以保证只有一个实例进行更新,避免数据不一致。 3. 分布式资源竞争:多个应用实例需要竞争某个资源,例如分布式锁可以用于实现分布式限流、分布式排他性操作等场景。 需要注意的是,分布式锁的实现需要考虑到高并发、死锁、误删等问题,需要根据具体的应用场景进行优化和测试。\[1\]\[2\]\[3\] #### 引用[.reference_title] - *1* *3* [Redis 分布式锁的实现原理和应用场景](https://blog.csdn.net/weixin_43025343/article/details/131081958)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down28v1,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [Redis实现分布式锁及其应用场景](https://blog.csdn.net/Crime11/article/details/130132324)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down28v1,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

maligebilaowang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值