对分布式系统而言,数据库的不一致是常态。
数据库的不一致主要原因
1.新功能比如你需要在原有业务基础上添加新功能新特性,数据库层面而言很可能是
添加了一个新字段,或者新表,对于之前的历史数据而言往往是没有的。假设你新加了一个字段表示状态,那么之前的状态应该是完成,但是如果现有逻辑去查,查到没有这个字段,很可能会于新逻辑中的状态为初始状态产生歧义。2.分布式系统的一致性本身难以解决分布式系统的一致性目前工业界而言,都是牺C保AP
。这意味着分布式系统的不一致是常态。
解决方案
面向不一致的coding具体而言主要有以下两个方面
1.首先对于数据的生产者而言,你的某个业务操作需要插入或者更新一系列表,由于没有采用分布式事务,当你的调用完成,通常你只能做到,如果整个方法返回成功,那么更新成功,如果返回失败或者抛出异常,你不能确保到底更新成功或者失败,你只能通过不断重试。当然连续的重试通常只能解决间歇性的失败,如短暂的网络问题,所以最好冲重试时还需要做退避。
假设原先的业务方法如下
onMessage(){
biz(){
insertTb1..
insertTb2..
...
insertTbN..
}
}
当然分布式系统都应该实现幂等性(借助数据的唯一索引),假设tb1,tb2..tbN上都存在唯一索引,如果只是如上所示的编码,那么如果在insertTbi(i<
N)时抛出异常,那么就会导致消息系统重试,并且在insertTb1时由于数据库唯一键存在就会抛出异常,一致性无法确保。
为什么会这样?
幂等性关心的数据的状态而非业务操作(insert),其实我们并不关心insert操作是成功or失败,
我们关心的数据库中的数据是否存在。所以有语义上的冲突,正确的方案应该是类似putIfAbsent语义。而非put语义。
改进后的方案如下
onMessage(){
biz(){
insertTb1IfAbsent..
insertTb2IfAbsent..
...
insertTbNIfAbsent..
}
}
SQL里面也有直接对应的语句如
- insert ignore just means insertIfAbsent
- replace into .. just means
- INSERT … ON DUPLICATE KEY UPDATE
- INSERT INTO table (a,b,c) VALUES (1,2,3)
ON DUPLICATE KEY UPDATE c=c+1;
- INSERT INTO table (a,b,c) VALUES (1,2,3)
但这种SQL往往开销较大,所以
insertIfAbsent最佳实践上采用的方案即==手动check==
insertIfAbsent(){
boolean exist=select();
if(! exist) then {
insert()
}
}
唯一的区别在于,后者无法保证==原子性==,select和insert存在竞态,但是此时会抛出异常,
但是由于可以重试,所以不影响主流程。
有没有可以优化的点?
- 比如如果检测出duplicate key也认为数据已经存在,这样就减少可重试理论上更加高效。
- jdbc而言应该是SQLIntegrityConstraintViolationException。
- com.mysql.jdbc则是MySQLIntegrityConstraintViolationException。
- spring dao将checked异常封装转换成unchecked异常应该是DataIntegrityViolationException。
所以又优化成以下代码
insertIfAbsent(){
boolean exist=select();
if(! exist) then
try{
insert()
}catch(DataIntegrityViolationException e){
//just ignore it
}
catch(Exception e){
//others just throw it
throw e;
}
}
}
2.对于数据的消费方而言,如果同一数据存在多个副本,那么也必须处理可能的不一致场景,比如某个类型字段存在两个表中,取的时候取哪个?冗余字段的场景实在是太常见了。
采用的方案
对外只提供一个服务接口,在实现内部去做merge
应用层面上模拟分布式事务。即进行到那个阶段