问题描述
插入并发控制工作中碰到的问题工作环境中ge_money_details是会员的金钱表判断其余额时使用selectsum(money)fromge_money_details获取增加金额时#{money}为正数减少金额时#{money}为负数用以下SQL进行并发插入出现金额被减少为负数的情况<selectid="InsertMoneyDetailsByAccumulate"resultType="java.lang.Long"parameterType="com.smart.gpo.model.po.GeMoneyDetails">INSERTINTOge_money_details(account_id,operate_type,money,create_user,create_time,update_user,update_time)select#{accountId},#{operateType},#{money},#{createUser},#{createTime},#{updateUser},#{updateTime}where(selectsum(money)fromge_money_detailswhereaccount_id=#{accountId})>=abs(#{money})returningmoney_details_id</select>
使用的是mybetis进行入库数据库为postgresql判断结果是以下代码List<Long>resultCount=this.getMybatis().getListByObject("InsertMoneyDetailsByAccumulate",geMoneyDetails);if(resultCount==null||resultCount.isEmpty())thrownewDaoException("余额不足!");
自测时插入成功resultCount的结果是money_details_id的值插入失败余额不足时候结果是resultCount.lenght=0正确的抛出的异常就是并发时出现了一次错误但没有复现出来后来由于是生产环境就先把方法加了一个锁先解决问题但这段代码哪里有错误还是很迷惑
解决方案
解决方案二:
看不懂,什么被减少为负数?
解决方案三:
引用1楼zhangsf1982的回复:
看不懂,什么被减少为负数?
就是公司的金钱流水表会员的金钱流动会在金钱流水添加一条记录如:会员存款100元记录中的money字段就为100会员购买商品消耗10元
解决方案四:
引用1楼zhangsf1982的回复:
看不懂,什么被减少为负数?
上面那个回复手抖了还没写完就发出去了不知道为啥还不能编辑的就是公司的金钱流水表会员的金钱流动会在金钱流水添加一条记录如:会员存款100元记录中的money字段就为100会员购买商品消耗10元记录中的money字段就为-10判断会员余额时就sum(money)获得9090就是会员的余额我在insert的时候判断了下如果这次插入的数据是负数就判断会员的余额必须比本次插入的负数的绝对值大(就是余额要大于本次消费的金额)但出现了余额小于商品金额的情况下成功购买了商品的情况简单讲就是一个10元的商品会员只有11元的余额会员购买该商品时不知道怎么回事并发了2个请求结果两个请求判断会员的金额都是大于10然后会员购买了2个10元的商品余额变为-9
解决方案五:
不知道是不是由于2个SQL并发且在事务中并未提交延迟提交后产生的问题?事物管理器用的org.springframework.orm.hibernate3.HibernateTransactionManager如果是有什么解决办法不?(纯SQL修改的方式)有没有高人来解答下疑惑....
解决方案六:
这个并发问题,我感觉啊,你在方法名加上synchronized,防止出现负数问题
解决方案七:
引用5楼tt2x77的回复:
这个并发问题,我感觉啊,你在方法名加上synchronized,防止出现负数问题
上面有说明的后来由于是生产环境就先把方法加了一个锁先解决问题主要是没搞明白为啥insertwhere没有效果加synchronized会非常影响性能锁粒度太大了我用了个ConcurrentHashMap作为锁对单个会员进行锁操作booleanflag=false;//用会员ID进行加锁StringmoneyLockkey=geMoneyDetails.getAccountId().toString();synchronized(LockUtil.MoneyLock){flag=LockUtil.MoneyLock.get(moneyLockkey,true);if(flag){LockUtil.MoneyLock.put(moneyLockkey,false);}}if(flag){try{//是扣除金额if(geMoneyDetails.getMoney().compareTo(BigDecimal.ZERO)==-1&&//因为回滚操作会导致负数回避掉回滚代码!geMoneyDetails.getOperateType().equals(OperateType.CancelBalance.getType())&&!geMoneyDetails.getOperateType().equals(OperateType.ReconstructionBet.getType())){//InsertMoneyDetailsByAccumulate就是insertwhere的那段代码List<Long>resultCount=this.getMybatis().getListByObject("InsertMoneyDetailsByAccumulate",geMoneyDetails);if(resultCount==null||resultCount.isEmpty())thrownewDaoException("余额不足!");}elsethis.getHibernate().save(geMoneyDetails);}finally{//释放锁信息LockUtil.MoneyLock.put(moneyLockkey,true);}}else{thrownewDaoException("您当前有金钱操作未完成请稍后尝试!");}
解决方案八:
引用6楼qqArz的回复:
Quote: 引用5楼tt2x77的回复:
这个并发问题,我感觉啊,你在方法名加上synchronized,防止出现负数问题上面有说明的后来由于是生产环境就先把方法加了一个锁先解决问题主要是没搞明白为啥insertwhere没有效果加synchronized会非常影响性能锁粒度太大了我用了个ConcurrentHashMap作为锁对单个会员进行锁操作booleanflag=false;//用会员ID进行加锁StringmoneyLockkey=geMoneyDetails.getAccountId().toString();synchronized(LockUtil.MoneyLock){flag=LockUtil.MoneyLock.get(moneyLockkey,true);if(flag){LockUtil.MoneyLock.put(moneyLockkey,false);}}if(flag){try{//是扣除金额if(geMoneyDetails.getMoney().compareTo(BigDecimal.ZERO)==-1&&//因为回滚操作会导致负数回避掉回滚代码!geMoneyDetails.getOperateType().equals(OperateType.CancelBalance.getType())&&!geMoneyDetails.getOperateType().equals(OperateType.ReconstructionBet.getType())){//InsertMoneyDetailsByAccumulate就是insertwhere的那段代码List<Long>resultCount=this.getMybatis().getListByObject("InsertMoneyDetailsByAccumulate",geMoneyDetails);if(resultCount==null||resultCount.isEmpty())thrownewDaoException("余额不足!");}elsethis.getHibernate().save(geMoneyDetails);}finally{//释放锁信息LockUtil.MoneyLock.put(moneyLockkey,true);}}else{thrownewDaoException("您当前有金钱操作未完成请稍后尝试!");}
比如你余额50,在19行,你执行一次取款50,这条数据插入了,可是数据库没有提交,你下次执行余额还是显示50你又插入了,我觉得你的insertwhere没问题
解决方案九:
synchronized可以锁具体对象,比如synchronized(geMoneyDetails.getMoney()),这样不会太影响性能
解决方案十:
1、有负数是正常的,电信运营商都能欠费,太严格的校验会增加负担2、应该实时计算当前最最新余额,而不是每次都sum
解决方案十一:
建议加一个UserBalance表来记录用户的余额消费时按UserId判断余额读取数据是加排他锁select×××forupdatewithRS