在具体实现上,我们是在用户启动快应用中心的时候触发一次同步操作,由客户端将OpenID和客户端本地标识提交到服务端进行绑定。服务端的绑定逻辑是:判断OpenID是否已经存在,如果不存在则插入数据库,否则更新对应数据行的local_identifier字段(因为用户可能先后在两个不同的手机上登录同一个vivo账号)。在后续的业务流程中,我们就可以根据OpenID查询对应的local_identifier,反之亦可。
但是代码上线一段时间后,我们发现t_account数据表中居然存在许多重复的OpenID记录。根据如上所述的绑定逻辑,这种情况理论上是不应该发生的。所幸这些重复数据并没有对更新和查询的场景造成影响,因为在查询的SQL中我们加入了LIMIT 1的限制,因此针对一个OpenID的更新和查询操作实际上都只作用于ID最小的那条记录。
于是,我们猜测问题的产生应该是由于并发请求造成的!我们模拟了客户端对接口的并发调用,确实出现了重复插入数据的现象,进一步证实了这个猜测的合理性。但是,明明客户端的逻辑是每个用户在启动的时候进行一次同步,为什么会出现同一个OpenID并发请求呢?
事实上,代码的实际运行并不如我们想象中的那么理想,计算机的运行过程中往往存在一些不稳定的因素,比如网络环境、服务器的负载情况。而这些不稳定因素就可能导致客户端发送请求失败,这里的“失败”可能并不意味着真正的失败,而是可能整个请求时间过长,超过了客户端设定的超时时间,从而被人为地判定为失败,于是通过重试机制再次发送请求。那么最终就可能导致同样的请求被提交了多次,而且这些请求也许在中间某个环节被阻塞了(比如当服务器的处理线程负载过大,来不及处理请求,请求进入了缓冲队列),当阻塞缓解后这几个请求就可能在很短的时间内被并发处理了。
这其实是一个典型的并发冲突问题,可以把这个问题简单抽象为:如何避免并发情况下写入重复数据。事实上,有很多常见的业务场景都可能面临这个问题,比如用户注册时不允许使用相同的用户名。
一般来说,我们在处理这类问题时,最直观的方式就是先进行一次查询,当判断数据库中不存在当前数据时才允许插入。
显然,这个流程从单个请求的角度来看是没有问题的。但是当多个请求并发时,请求A和请求B都先发起一次查询,并且都得到结果是不存在,于是两者都又执行了数据插入,最终导致并发冲突。
ALTER TABLE t_account ADD UNIQUE uk_open_id( open_id );
一旦为open_id列加上唯一索引后,当上述并发情况发生时,请求A和请求B中必然有一者会优先完成数据的插入操作,而另一者则会得到类似错误。因此,最终保证t_account表中只有一条openid=xxx的记录存在。
Error Code: 1062. Duplicate entry 'xxx' for key 'uk_open_id'
对于Java语言,大家最熟悉的锁机制就是synchronized关键字了。
public synchronized void submit(String openId, String localIdentifier){
Account account = accountDao.find(openId);
if (account == null) {
// insert
}
else {
// update
}
}
但是,事情可没这么简单。要知道,我们的程序可不是只部署在一台服务器上,而是部署了多个节点。也就是说这里的并发不仅仅是线程间的并发,而是进程间的并发。因此,我们无法通过java语言层面的锁机制来解决这个同步问题,我们这里需要的应该是分布式锁。
CREATE TABLE `myLock` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`method_name` varchar(100) NOT NULL DEFAULT '' COMMENT '锁定的方法名',
`value` varchar(1024) NOT NULL DEFAULT '锁信息',
PRIMARY KEY (`id`),
UNIQUE KEY `uidx_method_name` (`method_name `) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='锁定中的方法';
然后,我们就可以通过插入数据和删除数据的方式来实现加锁和解锁:
#加锁
insert into myLock(method_name, value) values ('m1', '1');
#解锁
delete from myLock where method_name ='m1';
基于数据库实现的方式虽然简单,但是存在一些明显的问题:
SET key value [EX seconds] [PX milliseconds] NX
解锁操作只需要:
DEL key
public class RedisLock {
private static final String LOCK_SUCCESS = "OK";
private static final String LOCK_VALUE = "lock";
private static final int EXPIRE_SECONDS = 3;
@Autowired
protected JedisCluster jedisCluster;
public boolean lock(String openId) {
String redisKey = this.formatRedisKey(openId);
String ok = jedisCluster.set(redisKey, LOCK_VALUE, "NX", "EX", EXPIRE_SECONDS);
return LOCK_SUCCESS.equals(ok);
}
public void unlock(String openId) {
String redisKey = this.formatRedisKey(openId);
jedisCluster.del(redisKey);
}
private String formatRedisKey(String openId){
return "keyPrefix:" + openId;
}
}
在具体实现上,我们设置了3秒钟的过期时间,因为被加锁的任务是简单的数据库查询和插入,而且服务器与数据库部署在同个机房,正常情况下3秒钟已经完全能够足够满足代码的执行。
事实上,以上的实现是一个简陋版本的Redis分布式锁,我们在实现中并没有考虑线程的可重入性,也没有考虑锁被其他进程误释放的问题,但是它在这个业务场景下已经能够满足我们的需求了。假设推广到更为通用的业务场景,我们可以考虑在value中加入当前进程的特定标识,并在上锁和释放锁的阶段做相对应的匹配检测,就可以得到一个更为安全可靠的Redis分布式锁的实现了。
当然,像Redission之类的框架也提供了相当完备的Redis分布式锁的封装实现,在一些要求相对严苛的业务场景下,我建议直接使用这类框架。由于本文侧重于介绍排查及解决问题的思路,因此没有对Redisson分布式的具体实现原理做更多介绍,感兴趣的小伙伴可以在网上找到非常丰富的资料。
public class AccountService {
@Autowired
private RedisLock redisLock;
public void submit(String openId, String localIdentifier) {
if (!redisLock.lock(openId)) {
// 如果相同openId并发情况下,线程没有抢到锁,则直接丢弃请求
return;
}
// 获取到锁,开始执行用户数据同步逻辑
try {
Account account = accountDao.find(openId);
if (account == null) {
// insert
} else {
// update
}
} finally {
// 释放锁
redisLock.unlock(openId);
}
}
}
我想为Heroku构建一个Rails3应用程序。他们使用Postgres作为他们的数据库,所以我通过MacPorts安装了postgres9.0。现在我需要一个postgresgem并且共识是出于性能原因你想要pggem。但是我对我得到的错误感到非常困惑当我尝试在rvm下通过geminstall安装pg时。我已经非常明确地指定了所有postgres目录的位置可以找到但仍然无法完成安装:$envARCHFLAGS='-archx86_64'geminstallpg--\--with-pg-config=/opt/local/var/db/postgresql90/defaultdb/po
我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i
尝试通过RVM将RubyGems升级到版本1.8.10并出现此错误:$rvmrubygemslatestRemovingoldRubygemsfiles...Installingrubygems-1.8.10forruby-1.9.2-p180...ERROR:Errorrunning'GEM_PATH="/Users/foo/.rvm/gems/ruby-1.9.2-p180:/Users/foo/.rvm/gems/ruby-1.9.2-p180@global:/Users/foo/.rvm/gems/ruby-1.9.2-p180:/Users/foo/.rvm/gems/rub
我的最终目标是安装当前版本的RubyonRails。我在OSXMountainLion上运行。到目前为止,这是我的过程:已安装的RVM$\curl-Lhttps://get.rvm.io|bash-sstable检查已知(我假设已批准)安装$rvmlistknown我看到当前的稳定版本可用[ruby-]2.0.0[-p247]输入命令安装$rvminstall2.0.0-p247注意:我也试过这些安装命令$rvminstallruby-2.0.0-p247$rvminstallruby=2.0.0-p247我很快就无处可去了。结果:$rvminstall2.0.0-p247Search
由于fast-stemmer的问题,我很难安装我想要的任何rubygem。我把我得到的错误放在下面。Buildingnativeextensions.Thiscouldtakeawhile...ERROR:Errorinstallingfast-stemmer:ERROR:Failedtobuildgemnativeextension./System/Library/Frameworks/Ruby.framework/Versions/2.0/usr/bin/rubyextconf.rbcreatingMakefilemake"DESTDIR="cleanmake"DESTDIR=
当我尝试安装Ruby时遇到此错误。我试过查看this和this但无济于事➜~brewinstallrubyWarning:YouareusingOSX10.12.Wedonotprovidesupportforthispre-releaseversion.Youmayencounterbuildfailuresorotherbreakages.Pleasecreatepull-requestsinsteadoffilingissues.==>Installingdependenciesforruby:readline,libyaml,makedepend==>Installingrub
有时我需要处理键/值数据。我不喜欢使用数组,因为它们在大小上没有限制(很容易不小心添加超过2个项目,而且您最终需要稍后验证大小)。此外,0和1的索引变成了魔数(MagicNumber),并且在传达含义方面做得很差(“当我说0时,我的意思是head...”)。散列也不合适,因为可能会不小心添加额外的条目。我写了下面的类来解决这个问题:classPairattr_accessor:head,:taildefinitialize(h,t)@head,@tail=h,tendend它工作得很好并且解决了问题,但我很想知道:Ruby标准库是否已经带有这样一个类? 最佳
我正在尝试使用boilerpipe来自JRuby。我看过guide从JRuby调用Java,并成功地将它与另一个Java包一起使用,但无法弄清楚为什么同样的东西不能用于boilerpipe。我正在尝试基本上从JRuby中执行与此Java等效的操作:URLurl=newURL("http://www.example.com/some-location/index.html");Stringtext=ArticleExtractor.INSTANCE.getText(url);在JRuby中试过这个:require'java'url=java.net.URL.new("http://www
我意识到这可能是一个非常基本的问题,但我现在已经花了几天时间回过头来解决这个问题,但出于某种原因,Google就是没有帮助我。(我认为部分问题在于我是一个初学者,我不知道该问什么......)我也看过O'Reilly的RubyCookbook和RailsAPI,但我仍然停留在这个问题上.我找到了一些关于多态关系的信息,但它似乎不是我需要的(尽管如果我错了请告诉我)。我正在尝试调整MichaelHartl'stutorial创建一个包含用户、文章和评论的博客应用程序(不使用脚手架)。我希望评论既属于用户又属于文章。我的主要问题是:我不知道如何将当前文章的ID放入评论Controller。
我有一个涉及多台机器、消息队列和事务的问题。因此,例如用户点击网页,点击将消息发送到另一台机器,该机器将付款添加到用户的帐户。每秒可能有数千次点击。事务的所有方面都应该是容错的。我以前从未遇到过这样的事情,但一些阅读表明这是一个众所周知的问题。所以我的问题。我假设安全的方法是使用两阶段提交,但协议(protocol)是阻塞的,所以我不会获得所需的性能,我是否正确?我通常写Ruby,但似乎Redis之类的数据库和Rescue、RabbitMQ等消息队列系统对我的帮助不大——即使我实现某种两阶段提交,如果Redis崩溃,数据也会丢失,因为它本质上只是内存。所有这些让我开始关注erlang和