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

 

我们可以考虑使用非阻塞方式,实现思路可以参考原子类的cas机制。即借助冲突检查机制判断在更新过程中是否存在来自其他线程的干扰。...





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

 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;i0){

break;

}

}

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

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

更多精彩:

java达人
ID:drjava
(长按识别)


    关注 java达人


微信扫一扫关注公众号

0 个评论

要回复文章请先登录注册