redis实现分布式锁---实操---问题解决

项目有一个留痕的操作,考虑到并发以及生产环境是多服务器的情况,决定使用分布式锁来保证并发下的正确性,由于是第一次做,除了一些问题,来记录一下:

【本人经验不足,如有错误,希望得到大神的指教】

项目的竞态问题是:
同一用户/同一股票信息,都只记录一次,并且要拿到用户和股票的id来记录流水表
就涉及到先判断后插入这种竞态操作
第一版的思路是:
用户进来后先去查一次数据库,

  • ​ 如果为空,就去获取锁,

    • ​ 如果获取到锁,就执行插入操作,插入完成后就释放锁;(获取锁就是插入一个代表当前用户的value值,释放就是删除掉这个值)
    • ​ 如果没有获取到锁就去get,如果get不到了对应的value,就说明插入操作已经完成,就再查一次数据库,获取id
  • ​ 如果不为空,说明数据库中有这条数据,就直接能够拿到id

第一版的问题:记录流水会有遗漏

发现是duplicate-key的原因(插入数据库的时候,唯一索引),这说明锁是不成功的

我的猜想是在这个位置:

 tempUser = daxinUserMapper.selectOne(user);
        if (tempUser == null) {
            if (lock(accountLockKey)) { 
            	插入操作
                释放锁
            }

如果同一个用户,同一时间进来了两次,两个tempUser都是null,然后A线程很快,马上获取到锁了,插入然后就释放了,然后B线程这时获取锁是能够获取到的,然后B线程又进行了一次插入操作,就报了这个错(查看日志也是这个样子)

第二版的思路是:

用户进来后,直接去redis(hash的结构)中,尝试获取锁,也就是进行一个putIfAbsent,给一个“lock”的标志位的操作

  • 如果put成功,说明用户是第一次来,那么就进行插入操作,并且插入后将id写到对应key的value中,并且不释放
  • 如果put不成功,尝试去get,获取对应key的value的值,
    • 如果值是lock,说明有人在操作,并且还没操作完,就过一会再来get,直到get到id的值
    • 如果值不是lock,说明获得的是id,那就直接拿到了(redis是单线程,所以不会出问题)

第一版的代码是:

@Service
@Slf4j
public class StockFlowService {

    @Resource
    private DaxinUserMapper daxinUserMapper;

    @Resource
    private DaxinStockMapper daxinStockMapper;

    @Resource
    private DaxinStockFlowMapper daxinStockFlowMapper;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource(name = StackFlowStream.FLOW_SAVE_SEND)
    private MessageChannel flowSaveQueue;

    private static final String ACCOUNT_LOCK_KEY_TEMPLATE = "editorial:newstock:fundaccount:%s:value";

    private static final String STOCK_LOCK_KEY_TEMPLATE = "editorial:newstock:stock:%s:value";

    /**
     * 验证手机号后 激活
     * <p>
     * 消费者
     *
     * @see StockFlowService#saveFlow(UserFlowVo)
     */
    public void store(UserFlowVo userFlowVo) {
        flowSaveQueue.send(MessageBuilder.withPayload(userFlowVo).build());
        log.info("send to save userFlow :{}", userFlowVo);

    }

    private Boolean lock(String key) {
        Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(
                key, "lock", 20, TimeUnit.SECONDS);
        return lock;
    }

    private void unLock(String key) {
        stringRedisTemplate.delete(key);
    }

    private void retryLockExist(int count, String lockKey) {
        try {
            while (count-- > 0) {
                Thread.sleep(1000);
                log.info("retry redis lock {}({})", lockKey, count + 1);

                if (!stringRedisTemplate.hasKey(lockKey)) {
                    break;
                }
            }
        } catch (InterruptedException e) {
        }
    }

    /**
     * 激活异步处理
     * <p>
     * 生产者
     *
     * @see com.glsc.editorial.stockflow.controller.StockFlowController#store(UserFlowVo)
     */
    public void saveFlow(UserFlowVo userFlowVo) {
        log.info("enter saveFlow:{}", userFlowVo);
        // 1. 存入用户信息
        log.info("处理用户信息...");
        StopWatch stopWatch = new StopWatch("store");
        stopWatch.start("user..");
        User user = User.builder().fundAccount(userFlowVo.getFundAccount())
                .assetProp(userFlowVo.getAssetProp())
                .build();
        Integer uid;
        User tempUser = null;
        String accountLockKey
                = String.format(StockFlowService.ACCOUNT_LOCK_KEY_TEMPLATE, userFlowVo.getFundAccount() + "|" + userFlowVo.getAssetProp());
        tempUser = daxinUserMapper.selectOne(user);
        if (tempUser == null) {
            if (lock(accountLockKey)) {
                log.info("获取到锁:{}", user.getFundAccount());
                try {
                    log.info("操作数据库,存入用户信息:{}", user);
                    daxinUserMapper.insert(user);
                    uid = user.getId();
                } finally {
                    log.info("释放锁");
                    unLock(accountLockKey);
                }
            } else {
                log.info("未获取到锁,开始等待:{}", user.getFundAccount());
                retryLockExist(3, accountLockKey);
                tempUser = daxinUserMapper.selectOne(user);
                uid = tempUser.getId();
            }
        } else {
            log.info("已有用户信息,获取用户id:{}", tempUser.getId());
            uid = tempUser.getId();
        }
        stopWatch.stop();

        // 2. 存入股票信息
        log.info("处理股票信息...");
        LocalDateTime now = LocalDateTime.now();
        String sign = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"))
                .toString() + "_" + userFlowVo.getFundAccount();
        stopWatch.start("stock..");
        List<Stock> stocks = userFlowVo.getStockFlowvos().stream().map(e -> {
            Stock stock = CopyMapping.INSTANCE.stockFlow2Stock(e);
            String stockLockKey =
                    String.format(StockFlowService.STOCK_LOCK_KEY_TEMPLATE, e.getStockCode() + "|" + e.getStockType());
            Stock modelStock = Stock.builder().stockCode(e.getStockCode())
                    .stockType(e.getStockType())
                    .build();
            Stock tempStock = null;
            tempStock = daxinStockMapper.selectOne(modelStock);
            if (tempStock == null) {
                if (lock(stockLockKey)) {
                    log.info("获取到锁:{}", e.getStockCode() + "|" + e.getStockType());
                    try {
                        // 同一股票信息只存一次
                        log.info("存入股票信息:{}", stock);
                        daxinStockMapper.insert(stock);
                    } finally {
                        log.info("释放锁");
                        unLock(stockLockKey);
                    }
                } else {
                    log.info("未获取到锁,开始等待:{}", stock.getStockCode());
                    retryLockExist(3, stockLockKey);
                    tempStock = daxinStockMapper.selectOne(modelStock);
                    stock.setId(tempStock.getId());
                }
            } else {
                log.info("已有股票信息,获取股票id:{}", tempStock.getId());
                stock.setId(tempStock.getId());
            }
            return stock;
        }).collect(Collectors.toList());
        stopWatch.stop();

        // 存入流水信息
        log.info("处理流水信息...");
        stopWatch.start("insert flow");
        Map<String, StockFlowVo> stockFlowVo = userFlowVo.getStockFlowvos().stream().collect(Collectors.toMap(e -> e.getStockCode() + "|" + e.getStockType(), Function.identity()));
        List<StockFlow> collect = stocks.stream().map(stock -> {
            StockFlowVo vo = stockFlowVo.get(stock.getStockCode() + "|" + stock.getStockType());
            return StockFlow.builder().stockId(stock.getId())
                    .userId(uid)
                    .gmtCreate(now)
                    .isSuccess(vo.getIsSuccess())
                    .actualCount(vo.getActualCount())
                    .sign(sign)
                    .remark(vo.getRemark())
                    .hasRight(vo.getHasRight())
                    .build();
        }).collect(Collectors.toList());
        log.info("存入流水信息{}", JSON.toJSONString(collect));
        daxinStockFlowMapper.insertList(collect);
        stopWatch.stop();
        log.info(stopWatch.prettyPrint());
    }
}

第二版的代码是:

service

@Service
@Slf4j
public class StockFlowService {

    @Resource
    private DaxinUserMapper daxinUserMapper;

    @Resource
    private DaxinStockMapper daxinStockMapper;

    @Resource
    private DaxinStockFlowMapper daxinStockFlowMapper;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource(name = SFSource.FLOW_SAVE_SEND)
    private MessageChannel flowSaveQueue;

    private static final String ACCOUNT_LOCK_KEY_TEMPLATE = "editorial:newstock:fundaccount:hash";

    private static final String STOCK_LOCK_KEY_TEMPLATE = "editorial:newstock:stock:hash";

    /**
     * 验证手机号后 激活
     * <p>
     * 消费者
     *
     * @see StockFlowService#saveFlow(UserFlowVo)
     */
    public void store(UserFlowVo userFlowVo) {
        flowSaveQueue.send(MessageBuilder.withPayload(userFlowVo).build());
        log.info("send to save userFlow :{}", userFlowVo);

    }

    private Boolean lock(String key, String hashkey) {
        Boolean lock = stringRedisTemplate.opsForHash().putIfAbsent(key, hashkey, "lock");
        return lock;
    }

    private Integer getId(String key, String hashKey) {

        while ("lock".equals(stringRedisTemplate.opsForHash().get(key, hashKey))) {
            try {
                Thread.sleep(1000);
                log.info("retry get uid {}", hashKey);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        return  Integer.parseInt(String.valueOf(stringRedisTemplate.opsForHash().get(key, hashKey)));
    }

    /**
     * 激活异步处理
     * <p>
     * 生产者
     *
     * @see com.glsc.editorial.stockflow.controller.StockFlowController#store(UserFlowVo)
     */
    public void saveFlow(UserFlowVo userFlowVo) {
        log.info("enter saveFlow:{}", userFlowVo);
        // 1. 存入用户信息
        log.info("处理用户信息...");
        StopWatch stopWatch = new StopWatch("store");
        stopWatch.start("user..");
        User user = User.builder().fundAccount(userFlowVo.getFundAccount())
                .assetProp(userFlowVo.getAssetProp())
                .build();
        Integer uid;
        String accountLockKey = userFlowVo.getFundAccount() + "|" + userFlowVo.getAssetProp();
        if (lock(ACCOUNT_LOCK_KEY_TEMPLATE,accountLockKey)){
            log.info("获取到锁:{}", accountLockKey);
            log.info("操作数据库,存入用户信息:{}", user);
            daxinUserMapper.insert(user);
            uid = user.getId();
            log.info("更新redis:{}", uid);
            stringRedisTemplate.opsForHash().put(ACCOUNT_LOCK_KEY_TEMPLATE, accountLockKey, uid.toString());
        }else {
            if ("lock".equals(stringRedisTemplate.opsForHash().get(ACCOUNT_LOCK_KEY_TEMPLATE,accountLockKey))){
                log.info("未获取到锁:{}", accountLockKey);
                log.info("等待,直到获取...");
                uid = getId(ACCOUNT_LOCK_KEY_TEMPLATE, accountLockKey);
            }else {
                log.info("已有用户信息,获取用户id:{}", accountLockKey);
                Object o = stringRedisTemplate.opsForHash().get(ACCOUNT_LOCK_KEY_TEMPLATE, accountLockKey);
                uid = Integer.parseInt(String.valueOf(o));
            }
        }
        stopWatch.stop();

        // 2. 存入股票信息
        log.info("处理股票信息...");
        LocalDateTime now = LocalDateTime.now();
        String sign = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"))
                .toString() + "_" + userFlowVo.getFundAccount();
        stopWatch.start("stock..");
        List<Stock> stocks = userFlowVo.getStockFlowvos().stream().map(e -> {
            Stock stock = CopyMapping.INSTANCE.stockFlow2Stock(e);
            String stockLockKey = e.getStockCode() + "|" + e.getStockType();
            Stock modelStock = Stock.builder().stockCode(e.getStockCode())
                    .stockType(e.getStockType())
                    .build();
            if (lock(STOCK_LOCK_KEY_TEMPLATE, stockLockKey)){
                log.info("获取到锁:{}", stockLockKey);
                log.info("操作数据库,存入股票信息:{}", modelStock);
                daxinStockMapper.insert(stock);
                log.info("更新redis:{}", stock.getId());
                stringRedisTemplate.opsForHash().put(STOCK_LOCK_KEY_TEMPLATE, stockLockKey, stock.getId().toString());
            }else {
                if ("lock".equals(stringRedisTemplate.opsForHash().get(STOCK_LOCK_KEY_TEMPLATE,accountLockKey))){
                    log.info("未获取到锁:{}", stockLockKey);
                    log.info("等待,直到获取id...");
                    stock.setId(getId(ACCOUNT_LOCK_KEY_TEMPLATE, accountLockKey));
                }else {
                    log.info("已有用户信息,获取用户id:{}", accountLockKey);
                    Object o = stringRedisTemplate.opsForHash().get(ACCOUNT_LOCK_KEY_TEMPLATE, accountLockKey);
                    stock.setId(Integer.parseInt(String.valueOf(o)));
                }
            }
            return stock;
        }).collect(Collectors.toList());
        stopWatch.stop();

        // 存入流水信息
        log.info("处理流水信息...");
        stopWatch.start("insert flow");
        Map<String, StockFlowVo> stockFlowVo = userFlowVo.getStockFlowvos().stream().collect(Collectors.toMap(e -> e.getStockCode() + "|" + e.getStockType(), Function.identity()));
        List<StockFlow> collect = stocks.stream().map(stock -> {
            StockFlowVo vo = stockFlowVo.get(stock.getStockCode() + "|" + stock.getStockType());
            return StockFlow.builder().stockId(stock.getId())
                    .userId(uid)
                    .gmtCreate(now)
                    .isSuccess(vo.getIsSuccess())
                    .actualCount(vo.getActualCount())
                    .sign(sign)
                    .remark(vo.getRemark())
                    .hasRight(vo.getHasRight())
                    .build();
        }).collect(Collectors.toList());
        log.info("存入流水信息{}", JSON.toJSONString(collect));
        daxinStockFlowMapper.insertList(collect);
        stopWatch.stop();
        log.info(stopWatch.prettyPrint());
    }
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值