加入收藏 | 设为首页 | 会员中心 | 我要投稿 核心网 (https://www.hxwgxz.com/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 移动互联 > 正文

纯技术干货分享:分布式事务处理方式总结

发布时间:2019-07-30 20:16:45 所属栏目:移动互联 来源:IT技术分享
导读:在项目开发中,经常会需要处理分布式事务。例如数据库分库分表之后,原来在一个单库上的操作可能会跨越多个数据库。系统服务化拆分之后,原来的在一个系统上的操作可能会跨越多个系统。就连我们平时经常使用到的缓存(如redis、memcache等)也可能涉及分布式
副标题[/!--empirenews.page--]

在项目开发中,经常会需要处理分布式事务。例如数据库分库分表之后,原来在一个单库上的操作可能会跨越多个数据库。系统服务化拆分之后,原来的在一个系统上的操作可能会跨越多个系统。就连我们平时经常使用到的缓存(如redis、memcache等)也可能涉及分布式事务,因为缓存和数据库是两个不同的实体,如何保证数据在缓存和数据库间的一致性也是要重点考虑的。分布式事务就是指事务要处理的资源分别位于分布式系统中的不同节点之上的事务。

对于单机系统,通常我们借助数据库实现本地事务,例如下面JDBC代码实现了一个事务:

  1. Connection con = datasource.getConnection(); 
  2. con.setAutoCommit(false); 
  3. ... 
  4. 执行CRUD操作,可能会涉及到多个表 
  5. ... 
  6. con.commit()/con.rollback() 

由于在分布式系统中,多个系统无法共用同一个数据库链接,所以无法简单借用上面的处理方式实现分布式事务。

下面将介绍几种本人在实际开发中使用过的处理分布式事务的方式,最后再引出分布式事务的相关理论并进行总结。

避免出现分布式事务

由于分布式事务比较难于处理,所以应该尽量避免分布式事务的发生。例如对于一个客户信息系统,由于注册用户数太多导致存储的数据量过大,所以对其进行分库分表存储。而客户信息模型又分为多个子模型,对应数据库中的多个表,例如客户基本信息表、客户登录账号表、客户登录密码表、客户联系方式表等等。假设登录账号表和客户基本信息表的关联关系如下所示:

纯技术干货分享:分布式事务处理方式总结

user_id和login_id分别是两个表的主键,user_id还作为login_info表的外键使两个表关联。在用户注册时会自动生成user_id和login_id的值。 user_info和login_info两个表分别采用user_id和login_id计算分库分表规则 。假设我们对每个模型分十库一百表存储,即存在user_info_00 ~ user_info_99一百个表,其中user_info_00 ~ user_info_09属于第一个库,user_info_10 ~ user_info_19属于第二个库,依次类推。

在分库分表之后,如果我们不仔细的考虑user_id和login_id的生成规则(例如随意生成一个数字字符串或简单使用递增sequence),就可能导致同一个用户的user_info信息和login_info信息被存储到两个不同的库,这就会导致分布式事务发生。

面对这种问题,最好的解决思路就是考虑如何避免分布式事务的发生。只要想办法让跟一个用户相关的所有模型数据全部存入到一个库中,就可以避免分布式事务了。由于每个模型数据的分库分表路由规则又是由各个表的主键id决定的(例如user_id、login_id),所以只要对各个表的主键生成规则进行定制,就可以保证一个用户的所有模型数据全部存到同一个库。假设有下面的id生成规则:

纯技术干货分享:分布式事务处理方式总结
  • 开始的两位是标识模型位,例如user_id以01开头,login_id以02开头。
  • 接下来的11位是sequence递增序列号,如果想要更多的ID可以扩大这部分的位数,但对于存储用户信息而言,11位的长度足够。
  • 接下来是分库分表位,如果每个模型的分库分表算法都相同,那么只要保证每个模型的主键ID的分库分表位都相同,就能保证一个用户的所有模型数据都会存到同一个库中。
  • 最后一位是id校验位,这一位根据前面15位的内容生成,方便对一个id进行校验。

根据这个思想,我们可以在用户注册的时候先生成user_id,user_id的分库分表位可以随机生成。然后在为其它模型生成主键id时(例如login_id),必须让这个模型的主键id的分库分表位与user_id的分库分表位相同。另外一点也要注意,一个表的查询条件不一定只有主键id一个,如果有其它查询条件列,那就要保证那一列的生成规则也要包含相同的分库分表位,否则就不能使用该列进行查询。

通过这种方式,就可以保证一个用户的所有模型数据全部存储到同一个库中,有效的避免分布式事务的发生。

事务补偿

通常情况下,应对高并发的一个主要手段就是增加分布式缓存(如redis)以提高查询性能。增加分布式缓存后系统查询数据的流程如下图:

纯技术干货分享:分布式事务处理方式总结

即先尝试从缓存中查询数据,如果缓存命中就直接返回结果,否则尝试从DB中查询数据。如果查询DB命中则将数据补充到缓存,以备下次查询时可以命中缓存。

而在更新数据时,通常是先更新DB中的数据,DB写入成功后再更新缓存中的数据。那么就有一个问题, 如何保证缓存和DB间数据的一致性? 由于缓存和DB是两个不同的实体,写入DB成功后再去更新缓存,如果缓存更新失败(例如网络抖动造成短暂的缓存不可用)就会造成缓存和DB的不一致。此时按照上图的查询逻辑,先查缓存就会查询到“脏”的数据,就会严重影响业务。这也是一个典型的分布式事务问题——缓存和DB要嘛同时更新成功,要嘛同时更新失败。解决这个问题的一个较好方式就是事务补偿。

我们可以在DB中创建一张事务补偿表transaction_log,transaction_log表可以和业务数据在一个库中,也可以在不同的库。在更新数据前,先将要更新的模型数据记录到transaction_log中。例如我们更新user_info表中的数据,就将userId记录到transaction_log中。

transaction_log记录成功后,再去更新业务数据表user_info中的内容,最后更新缓存中的userInfo数据。缓存更新成功后,就可以删除transaction_log表中对应的记录。

假设在更新完user_info表之后,由于网络抖动等原因导致缓存更新失败,则transaction_log表中对应的记录就会一直存在,表示这个事务没有完成的一种记录。

应用会创建一个定时任务,周期性的扫描transaction_log表中的记录(例如每隔2S扫描一次)。发现有符合条件的记录,就尝试执行补偿逻辑。例如更新用户信息时,DB中的user_info表更新成功,但缓存更新失败,定时任务发现transaction_log表中对应的记录没有删除且已经超过正常等待时间,就尝试使缓存和DB一致(可以删除缓存中对应的数据,也可以根据userId重新查询DB再补充的缓存)。补偿任务执行完成后,就可以删除transaction_log表中对应的记录。如果补偿任务执行再次失败,就保留transaction_log表中的记录,等待下个周期再次执行。

(编辑:核心网)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

热点阅读