并发编程的两个案例

并发编程一直是开发过程中非常有挑战力的部分,不仅需要保证数据的正确性,也要考虑这个性能是可以接受的,既然已经考虑并发这个事情,说明它的瞬间流量已经不低了。我们一般通过加锁的方式保证数据的正确性,那这个加锁的粒度跟时长。就必须要去做取舍。结合两个案例,去讲解并发编程的难点。

**案例一:**某个社区系统,社区账号是付费的,所以存在一种可能,一个用户买了一个账号就不停的给别人去使用,怎么去做风控呢,用户切换ip,系统会发一个通知,做一个操作记录。为了不影响主链路的可用性和耗时,这里使用了异步化的操作。

用户登陆进来之后就会判断这个当前ip和表里面这个ip是不是一样的,不一样的话就会把数据库里面的ip更新为当前ip,因为写这个操作日志和更新这个ip是一定要一个原子操作的,所以加了事务控制。

	@Transactional
    @Async("commonAsyncThreadPool")
    public void updateIp(Long userId,String ip){
        String lockKey = userId.toString() + "_"+ip;
        Boolean lock = distributedLock.lock(lockKey,10, TimeUnit.SECONDS);
		if(!lock) return;
        try {
            UserDO userDO = userDAO.getById(userId);
            if(!StringUtils.isEmpty(userDO.getIp()) && userDO.getIp().equals(ip)) return;
            OperateLogDO operateLogDO = new OperateLogDO();
            operateLogDO.setType(OperateTypeEnum.CHANGE_IP.getCode());
            operateLogDO.setUserId(userId);
            operateLogDO.setType(OperateTypeEnum.CHANGE_IP.getCode());
            operateLogDAO.inseret(operateLogDO);
            userDAO.updateIp(userDO.getId(),ip);

            monitorAbility.sendMsg(userDO.getName() + "切换ip" + userDO.getIp + "->" + ip);
        } finally {
            distributedLock.unlock();
            //locate-1  wait。。
        }


    }

那这个调用的地方,已经登陆的所有用户都会走到这个切面

@Around("@annotation(com.aka.aspect.LoggedCheck)")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
        Cookie[] cookies = httpServletRequest.getCookies();
        String ip = requestUtils.getCurrentIp();
        if(cookies == null){
            //。。。
        }
        String token = null;
        for(Cookie cookie : cookies){
            if(cookie.getName().equals(Constants.TOKEN)){
                token = cookie.getValue();
                break;
            }
        }
        if(token == null){
            //。。。
        }
        UserDO userDO = userDAO.getByToken(token);
        if(userDO == null){
            //。。。
        }
        userService.updateIp(userDO.getUserId(),ip);
        return proceedingJoinPoint.proceed();
    }

那么这里为什么要去防止并发呢,因为一个页面加载,他可能会发起多个ajax请求,如果两个请求同时过来,都去查当前数据库,数据库里面的是老ip,都去发这个消息,虽然更新ip没问题,但是更新日志会写多条。

这里通过用户id加ip,使用分布式锁进行加锁,释放锁。

这里会出现问题一,当我们在locate-1处等了一时间,那么锁已经释放了,但是方法没有执行结束,事务尚未提交。当有t2线程进来,由于t1线程已经把锁释放掉了,t2线程是能获得所得,那么他拿到的一定是老的ip。所以还是会走下面的逻辑,重复发监控告警消息,这种并发确实发生比较极端但是确实是存在的。

问题二另外发送消息下面如果有别的业务发生异常了,消息是不支持回滚的。

针对以上问题对代码进行修改,取消声明式事务,使用编程式事务,这样既保证事务粒度很小,减少对数据库连接池的消耗,事务提交之后采取发送消息,才去释放锁。

@Async("commonAsyncThreadPool")
    public void updateIp(Long userId,String ip){
        String lockKey = userId.toString() + "_"+ip;
        Boolean lock = distributedLock.lock(lockKey,10, TimeUnit.SECONDS);

        try {
            if(!lock) return;
            UserDO userDO = userDAO.getById(userId);
            if(!StringUtils.isEmpty(userDO.getIp()) && userDO.getIp().equals(ip)) return;
            OperateLogDO operateLogDO = new OperateLogDO();
            operateLogDO.setType(OperateTypeEnum.CHANGE_IP.getCode());
            operateLogDO.setUserId(userId);
            operateLogDO.setType(OperateTypeEnum.CHANGE_IP.getCode());
            
            transactionTemplate.execute((status)->{
                operateLogDAO.inseret(operateLogDO);
                userDAO.updateIp(userDO.getId(),ip);
                return null;
            });
            monitorAbility.sendMsg(userDO.getName() + "切换ip" + userDO.getIp + "->" + ip);
        } finally {
            distributedLock.unlock();
        }


    }

案例二:某个社区系统,开发了一个聊天室,聊天室里面会发各种消息,每个用户他上个消息读到什么位置会记录一个偏移量,能够实现,他下一次进来这个聊天室的时候跳转到什么地方。把偏移量存在user表的remark字段里面,用一个json存起来的,还有用户的其他配置信息。

public void refreshReadMsgOffset(UserDO userDO,long msgId){
        if(StringUtils.isEmpty(userDO.getRemark())) return;
        UserRemarkBO userRemarkBO = JSON.parseObject(userDO.getRemark(),UserRemarkBO.class);
        if(userRemarkBO.getOffsetId() >= msgId) return;

        String key = LockKeyConstants.UPDATE_MSG_OFFSET_PREFIX + userDO.getId();
        Boolean locked = distributedLock.lock(key,10,TimeUnit.SECONDS);

        try {
            userRemarkBO.setMsgOffsetId(msgId);
            userDO.updateRemark(userDO.getId(),JSON.toJSONString(userRemarkBO));
        } finally {
            distributedLock.unlock();
        }


    }

这个案例可能出现的问题,T1线程读取的偏移量是1,更新为2,T2线程读取的偏移量是1,获取锁更新为3,释放锁,T1线程获取锁更新为2,这时候预期是3,实际是2。这种是存在时序上的并发的,因为从浏览器到整个前置的链路,并没有保证T1线程相应结束之后再去发起T2线程,这就导致时序上的并发,所以这里使用double check的方式校验。

public void refreshReadMsgOffset(UserDO userDO,long msgId){
        if(StringUtils.isEmpty(userDO.getRemark())) return;
        UserRemarkBO userRemarkBO = JSON.parseObject(userDO.getRemark(),UserRemarkBO.class);
        if(userRemarkBO.getOffsetId() >= msgId) return;

        String key = LockKeyConstants.UPDATE_MSG_OFFSET_PREFIX + userDO.getId();
        Boolean locked = distributedLock.lock(key,10,TimeUnit.SECONDS);

        try {
            userRemarkBO.setMsgOffsetId(msgId);
            userDO.updateRemark(userDO.getId(),JSON.toJSONString(userRemarkBO));
            if(userRemarkBO.getOffsetId() >= msgId) return;
            userRemarkBO.setMsgOffsetId(msgId);
            userDO.updateRemark(userDO.getId(),JSON.toJSONString(userRemarkBO));
        } finally {
            distributedLock.unlock();
        }


    }

也可以在查的时候就加分布式锁,但是这样会扩大锁得范围,对性能上是不好的。我们在看代码的时候是顺序的静态的,考虑到并发问题的时候就要考虑到动态的,时序的。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值