擦,突然明白为啥会在两个上面都会执行了,还是两个实例都是连的同一个数据库,就像job刚提交到db之后,谁先拿到谁就执行一样,只不过这个粒度是job。
我们知道,还有一个比job更小的粒度,叫task,这个task有个自己的状态,对于提交到库里的task来说,任何一个实例也都可以拿到,就是说在DO状态(子状态)可能做了互斥,在LISTEN状态(子状态)谁拿到谁就执行,谁先执行谁就会把task的状态(主状态)设置为SUCCESS,而检测到SUCCESS说明执行成功,不必执行了。
因此,在一个具体的task当中,execute()方法是用来执行的,也就是说执行一些比如创建IAAS资源、写库等操作,而listen()方法是用来检测异步任务执行的情况的,因此listen方法中只能做“读”操作,即readonly,如果要在listen里面做写的操作,就会有问题!!
如果非得在listen中写,比如创建共享盘,查询创盘job成功之后,我们才往db中插入一条记录(写的动作),由于在多个实例的情况下,可能会同时往数据库中插入记录(后面会详细解释为什么会有这种情况发生),这个时候主键是volumeId,先插库的成功,后插库的也会报“主键重复”的错误——我们在生产环境上就遇到了这个问题,一共两次。头一次是我自己发现的,那时候只看了一个实例的日志,还曾深度怀疑是MySQL数据库抽风了,尝试去分析binlog,将二进制转换成了sql语句明文,最后也没找出来异常。。。好不容易找到了公司的一位大牛,要我把整个数据发给他,大咖最终也没能通过“共享屏幕”的方式高抬贵手,最后无疾而终。第二次是生产环境的CI跑出来的,由于我们刚刚“解决”了一个非常“诡异”的问题(这个让我们几位同时百思不得其解的“诡异的问题”就是,如果有多个实例连接同一个db,异步job就会执行串,比如a实例会执行b实例提交的job,因为一旦job被提交到db之后,谁先拿到谁就执行),因此下意识的就去另外一个实例上去搜日志,果然,被另外一个实例以不到0.3秒的时间优势(几乎等同于同一时间查到的job执行成功)优先插库,导致本实例报“主键重复插入”的错误,导致创建共享盘失败,导致创建文件系统失败。
既然问题的根因确定了,那么解决的方法就很多,也很容易——没有解决不了的问题,只有不能被明确的问题!!
我们采用最容易的一种,既然问题是重复插入,那么不重复插入就能解决问题,我们的ORM用的是Hibernate,重复插入是因为直接用了save,改成用saveOrUpdate就好。
好,继续~
为什么在多个实例的情况下,可能会同时往数据库中插入记录?
其实上面已经说了,task的listen方法里面的逻辑有可能被连接同一db的多个实例执行的,这个时候,如果listen中有插库的逻辑,就会出现问题中的现象。
为什么db中的job记录中明明有执行机的hostname,还会在另外一个实例执行呢?
因为,实例和实例之间互相不认识,也就是独立的,无任何耦合关系,一个实例挂了,另外的实例是感知不到的,然而我们的db并没有挂,我们也就是挂了一个实例而已,既然另外的实例好好的,为什么不让它继续执行呢??对吧?!
引申一下,为什么这样设计呢?
因为,在分布式的网络环境下,谁知道某个实例啥时候挂呢,那么就要假设某个实例随时都有可能会挂,来设计系统咯!
还有一个问题待解释,既然多个实例之间是独立的,我们又是怎么感觉“实例之间共享内存”呢?
回到上面重复插库的问题,既然能够跑到重复插库,那就只能有一种情况,就是这两个实例去查询同一个创盘的job,返回的是同一个volumeId,不对啊,创盘的job存在了context里面,这个context不应该在内存里面吗??
擦,打字打到这里,突然灵光一现,又冒出来一个惊天大发现(为啥是又,因为多实例连同一个db会跑串,也是我发现的,那个问题困扰了我们的几个同事好久,明明修改了重试次数,可是重试次数还是和没修改一样,原因就是还有另外一个实例连着同一个db,而另外一个实例还是老代码,即未修改的重试次数),那就是一个属性存到了context之后,context后台又把这个新的属性同步刷新到了db中,这样,另外一个实例就能拿到了!!——如果排除“共享内存”,那也就只能共享db了,回工位验证一下!!
突然,一道光,豁然开朗!
突然觉得,分布式系统好难啊,这才有两个实例就要考虑这么多事情!这样就能更加明白,微服务架构下这什么要有服务发现、网关、路由、断路器、客户端负载均衡等等一系列的功能了。
手机码字,回见。












网友评论