微信号:java_daren

介绍:精通java技术;具备互联网思维,进可创业,退可求职谋生,本号正是为了召集和培养这样的达人.

一段简单代码在并发环境下的优化思路

2018-06-02 09:09 java达人


有一段简单的代码,主要功能是根据好友的注册等活动来计算邀请者本人的贡献分,因此,每次有新的好友参与活动都会触发下面的方法:


 public void  calculateIScore(String friendId){ 
  //获取邀请者本人    Person person = personMapper.selectInviter(friendId);
 
  //根据相关好友活动计算邀请者贡献分    int score= calculateScoreByAllFriendAct(person)    person.setScore(score)    personMapper.updateScore(person); }

一开始,并发量不高,几乎每次运行都是单个线程,因此计算得出的分数都是正确的。但随着用户邀请量的激增,以及好友活动记录的频繁插入,使这个方法时常暴露在并发环境下。这就导致了一个问题:


假设好友A先注册,然后好友B注册,几乎同时触发了calculateIScore方法:


好友A先注册

好友B后注册

注册成功


触发方法 calculateIScore()


获取邀请者,计算设置分数

 int score=calculateScoreByFriendAct(person)


 person.setScore(score)



注册成功


触发方法 calculateIScore()


获取邀请者,计算设置分数

  int score= calculateScoreByFriendAct(person)


    person.setScore(score)


更新贡献分

personMapper.updateScore(person);

更新贡献分

personMapper.updateScore(person);



这就导致根据共享数据计算数据值的时候,旧值覆盖最新值的现象,用户总是抱怨贡献分有时候会突然减少。


怎么改?一个简单的思路就是加独占锁。


比较通用的加锁方式是对数据库记录加行锁,并且配置事务。


  @Transaction
   public void  calculateIScore(String friendId){  
 
  //获取邀请者本人,Mapper文件中sql语句改为select from ...for update 形式。    Person person = personMapper.selectInviterForUpdate(friendId);  
 
   //根据相关好友活动计算邀请者贡献分    int score= calculateScoreByFriendAct(person)    person.setScore(score)    personMapper.updateScore(person);   }


这样无论是在单个服务还是多个服务部署环境下,都可以实现分布式锁的效果。


在单服务环境下,简单地加个锁也可以。


   
    public void  calculateIScore(String friendId){  
       
    synchronized(this){      Person person = personMapper.selectInviter(friendId);
          
    //根据相关好友活动计算邀请者贡献分      int score= calculateScoreByFriendAct(person)      person.setScore(score)      personMapper.updateScore(person);     }    }


关于分布式锁,还可以借助zookpeeper,redis等组件实现。


zookeeper分布式锁可参考早期文章

ZooKeeper构建分布式锁(选译)


redis锁实现思路很多,如锁命令INCR,如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCR 操作进行加一。 然后其它用户在执行 INCR 操作进行加一时,如果返回的数大于 1 ,说明这个锁正在被使用当中。


独占锁相对比较安全,但严重影响性能,线程阻塞和唤醒的开销都很大。


因此我们可以考虑使用非阻塞方式,实现思路可以参考原子类的cas机制。即借助冲突检查机制判断在更新过程中是否存在来自其他线程的干扰,如果存在,操作失败,且可以重试。CAS指令需要有3个操作数,分别是内存位置(在Java中可以简单理解为变量的内存地址,用V表示)、旧的预期值(用A表示)和新值(用B表示)。CAS指令执行时,当且仅当V符合旧预期值A时,处理器用新值B更新V的值,否则它就不执行更新,上述的处理过程是一个原子操作。


参考它的实现思路,我们可以给表加个版本号,查询时会取得当前记录的版本号,当更新时在where条件中判断版本号是否发生了变化,并且将版本号加1,如果更新失败,则重试,这里考虑可以使用自旋机制。


  public void  calculateIScore(String friendId){   
 
  //重试10次  for(int i=0;i<10;i++){    Person person = personMapper.selectInviter(friendId);  
 
  //根据相关好友活动计算邀请者贡献分    int score= calculateScoreByFriendAct(person)    person.setScore(score)    
 
//相应sql语句改为 //update person set version=version+1 ... where id=#{id} and version=#{version}形式    int updateNum = personMapper.updateScoreByIdAndVersion(person);    
   if(updateNum>0){    
     break;    }
       }  }

这样,更新时如果发现版本号变化,说明其他线程已经对记录作了更新操作,重试,再次计算得出最新值。


这里只提供一些思路,具体编码的时候还有很多要注意的地方,各位看官有类似的经验欢迎留言。



更多精彩:


java达人

ID:drjava

(长按识别)

  


 
java达人 更多文章 技术组长如何组织一次晨会(附面试考察标准) 共识算法三巨头的碰面 中高级技术面试考察过程中的关键点 程序员的短板及其克服之道 一段解决kafka消息处理异常的经典对话
猜您喜欢 【827】产品需求文档怎么写? 灰度发布系统的实现(续) 2018年开发者不可错过的开源工具 —— Android 篇 DevOps探究(二)| DevOps文化如何促进业务提升? 计算机求导的四种方法