Wetts's blog

Stay Hungry, Stay Foolish.

0%

读书笔记 - Java 多线程编程实战指南


第三张 Immutable Object(不可变对象)模式

模式简介

  • 多线程共享变量的情况下,为了保证数据一致性,往往需要对这些变量的访问进行加锁。这些同步访问控制,如显式锁(Explicit Lock)和 CAS(Compare and Swap)操作,会带来额外的开销和问题,如上下文切换、等待时间和 ABA 问题等。
  • Immutable Object 模式使得我们可以在不使用锁的情况下,即保证共享变量访问的线程安全,又能避免引入锁可能带来的问题和开销。
  • Immutable Object 模式的意图是通过使用对外可见的状态不可变的对象,使得被共享的对象“天生”具有线程安全性,而无须额外地同步访问控制。
  • 所谓状态不可变的对象,即对象一经创建,其对外可见的状态就保持不变。
阅读全文 »

tomcat配置项目的图片路径不在项目下的处理

1
2
3
4
5
6
7
8
9
10
11
12
13
<Host appBase="webapps" autoDeploy="true" name="localhost" unpackWARs="true" xmlNamespaceAware="false" xmlValidation="false">

<Context docBase="weibo" path="/weibo" reloadable="true" source="org.eclipse.jst.jee.server:weibo"/>
<!-- 设置图片虚拟路径[访问时路径为/static_img] -->
<Context path="/static_img" docBase="F:\temp" reloadable="false" />
</Host>
<!-- 也可以这样设置图片虚拟路径 -->
<Host name="10.0.0.123" appBase="webapps"
unpackWARs="true" autoDeploy="true"
xmlValidation="false" xmlNamespaceAware="false">

<Context path="" docBase="F:\temp" reloadable="false" ></Context>
</Host>

转自:https://blog.csdn.net/zwl156135995/article/details/51445862

作用:

select for update 是为了在查询时,避免其他用户以该表进行插入,修改或删除等操作,造成表的不一致性。

给你举几个例子:

  • select * from t for update 会等待行锁释放之后,返回查询结果。
  • select * from t for update nowait 不等待行锁释放,提示锁冲突,不返回结果
  • select * from t for update wait 5 等待5秒,若行锁仍未释放,则提示锁冲突,不返回结果
  • select * from t for update skip locked 查询返回查询结果,但忽略有行锁的记录

SELECT…FOR UPDATE 语句的语法如下:

1
2
3
4
  SELECT ... FOR UPDATE [OF column_list][WAIT n|NOWAIT][SKIP LOCKED];
其中:
  OF 子句用于指定即将更新的列,即锁定行上的特定列。
  WAIT 子句指定等待其他用户释放锁的秒数,防止无限期的等待。

“使用FOR UPDATE WAIT”子句的优点如下:

  1. 防止无限期地等待被锁定的行;
  2. 允许应用程序中对锁的等待时间进行更多的控制。
  3. 对于交互式应用程序非常有用,因为这些用户不能等待不确定
  4. 若使用了 skip locked,则可以越过锁定的行,不会报告由 wait n 引发的‘资源忙’异常报告

补充几点:

分成两类:加锁范围子句和加锁行为子句

  • 加锁范围子句:在 select…for update 之后,可以使用 of 子句选择对 select 的特定数据表进行加锁操作。默认情况下,不使用 of 子句表示在 select 所有的数据表中加锁
  • 加锁行为子句:当我们进行 for update 的操作时,与普通 select 存在很大不同。一般 select 是不需要考虑数据是否被锁定,最多根据多版本一致读的特性读取之前的版本。加入 for update 之后,Oracle 就要求启动一个新事务,尝试对数据进行加锁。如果当前已经被加锁,默认的行为必然是 block 等待。使用 nowait 子句的作用就是避免进行等待,当发现请求加锁资源被锁定未释放的时候,直接报错返回。

在日常中,我们对 for update 的使用还是比较普遍的,特别是在如 pl/sql developer 中手工修改数据。此时只是觉得方便,而对 for update 真正的含义缺乏理解。

For update 是 Oracle 提供的手工提高锁级别和范围的特例语句。Oracle 的锁机制是目前各类型数据库锁机制中比较优秀的。所以,Oracle 认为一般不需要用户和应用直接进行锁的控制和提升。甚至认为死锁这类锁相关问题的出现场景,大都与手工提升锁有关。

所以,Oracle 并不推荐使用 for update 作为日常开发使用。而且,在平时开发和运维中,使用了 for update 却忘记提交,会引起很多锁表故障。那么,什么时候需要使用 for update?就是那些需要业务层面数据独占时,可以考虑使用 for update。场景上,比如火车票订票,在屏幕上显示邮票,而真正进行出票时,需要重新确定一下这个数据没有被其他客户端修改。所以,在这个确认过程中,可以使用 for update。这是统一的解决方案方案问题,需要前期有所准备。


转自:https://blog.csdn.net/Victor_Cindy1/article/details/78142956

问题是:如果多线程之下同时查询一条数据查不到,则去插入,插入的时候也是多线程插入。

1
所以想到用 select for update 来控制不允许多线程插入(其实这种方式并没有解决问题)

先来了解几个名词

  • statement:一个 SQL 语句。
  • session:一个由 ORACLE 用户产生的连接,一个用户能产生多个 SESSION,但相互之间是独立的。
  • transaction:所有的改动都能划分到 transaction 里,一个 transaction 包含一个或多个 SQL。当一个 SESSION 建立的时候就是个 TRANSACTION 开始的时刻,此后 transaction 的开始和结束由 DCL 控制,也就是每个 COMMIT/ROLLBACK 都标示着一个 transaction 的结束。

用法介绍:

select … for update 会 LOCK 相应的 ROW。

只有一个 TRANSACTION 可以 LOCK 相应的行,也就是说如果一个 ROW 已经 LOCKED 了,那就不能被其他 TRANSACTION 所 LOCK 了。

LOCK 由 statement 产生但却由 TRANSACTION 结尾(commit,rollback),也就是说一个 SQL 完成后 LOCK 还会存在,只有在 COMMIT/ROLLBACK 后才会师释放。

使用这个行锁的情况一般是对并发的情况要求比较高的时候,需要锁住某行进行一些更新语句之后进行释放,再让其他 transaction 去操作。很好的利用了这点解决了并发的问题。数据库中锁类型:有两种基本的锁类型,排它锁(Exclusive Locks,即 X 锁)和共享锁(Share Locks,即 S 锁)。当数据对象被加上排它锁时,其他的事务不能对它读取和修改。加了共享锁的数据对象可以被其他事务读取,但不能修改。数据库利用这两种基本的锁类型来对数据库的事务进行并发控制。

Oracle 给 Select 结果集加锁,Skip Locked(跳过加锁行获得可以加锁的结果集)

for update 后面还可以跟着 [OF cols] [NOWAIT]

of 的使用主要是针对多表关联的时候,如果不使用of,对两个表涉及到的行都将锁住,使用of可以指定锁定哪个表,

例如:select a.MOBILE,b.NAME from connector a,student b where a.STU_ID=b.ID and a.MOBILE='13937134399' for updata of a.MOBILE

这样的话student表中对应的行是不加锁的,对connector一个表中行加锁,不使用两个表都加锁。

[NOWAIT]的使用是当锁冲突的时候提示的情况:

当有 LOCK 冲突时会提示错误并结束 STATEMENT 而不是在那里等待。返回错误是 “ORA-00054: resource busy and acquire with NOWAIT specified”,如果不使用就会一直等待,直到锁释放之后执行。在页面上调试的时候由于异常处理不好,把数据锁住了没有提交,也没有 rollback,遇到这样的情况的时候可以通过以下方式解决:

—–查看被锁对象的序列号、sid

1
2
3
4
5
6
7
8
9
SELECT o.owner,o.object_name,o.object_type,s.sid,s.serial# 
FROM v$locked_object l,dba_objects o,v$session s
WHERE l.object_id=o.object_id
AND l.session_id=s.sid
ORDER BY o.object_id,xidusn DESC
/
------利用sid 和序列号删除
alter system kill session '243,10265';
243是sid 10265是序列号

效果图演示

开两个窗口:

  1. 第一个窗口用 select for update 查询
    2

  2. 在另一个窗口用同样的语句查询,会显示一直在查询…说明被 block 了
    3

  3. 点击第一个窗口里的提交事务按钮,另一个窗口可以立刻获取查询结果
    4

继续回到我们开始提出的问题,为什么用 for update 不可以避免多线程插入的问题,因为:select for update 只能针对已经存在的数据进行加排他锁,如果查询的数据是 null,根本就不存在所谓的锁了。其中之一的解决办法是:加上唯一性约束条件。我们就给 OPEN_UID 和 LOGIN_TYPE 一起加唯一性约束 unique。所以数据库会保证只有一个唯一确定的记录,当两个请求同时向数据库插入相同的 OPEN_UID 和 LOGIN_TYPE 时,会采用抢占式插入,谁先插入其他方就不能再插入数据。

就会报唯一约束的异常:

1
2
3
4
5
6
7
8
9
### 
### ### Error updating database. Cause: java.sql.SQLIntegrityConstraintViolationException: ORA-00001: unique constraint (SLPROD.SYS_C0019132) violated
### The error occurred while setting parameters
### SQL: insert into SL$THIRD_LOGIN (OPEN_UID, REQUEST_SOURCE, LOGIN_TYPE, BIND_AID, NICK_NAME, ACCESS_TOKEN, EXPIRES_IN, UNION_ID) values ( ?, ?, ?, ?, ?, ?, ?, ? )
### Cause: java.sql.SQLIntegrityConstraintViolationException: ORA-00001: unique constraint (SLPROD.SYS_C0019132) violated
; SQL []; ORA-00001: unique constraint (SLPROD.SYS_C0019132) violated
; nested exception is java.sql.SQLIntegrityConstraintViolationException: ORA-00001: unique constraint (SLPROD.SYS_C0019132) violated
... 86 more
Caused by: java.sql.SQLIntegrityConstraintViolationException: ORA-00001: unique constraint (SLPROD.SYS_C0019132) violated

  • UTC是我们现在用的时间标准

  • UTC是根据原子钟来计算时间

  • GMT是老的时间计量标准。

阅读全文 »

当我们执行 Maven 构建命令,Maven 依赖库按以下顺序进行搜索:

第1步 - 搜索依赖本地资源库,如果没有找到,跳到第2步,否则,如果找到那么会做进一步处理。

第2步 - 搜索依赖中央存储库,如果没有找到,则从远程资源库/存储库中,然后移动到步骤4,否则如果找到,那么它下载到本地存储库中,以备将来参考使用。

第3步 - 如果没有提到远程仓库,Maven 则会停止处理并抛出错误(找不到依赖库)。

第4步 - 远程仓库或储存库中的搜索依赖,如果找到它会下载到本地资源库以供将来参考使用,否则 Maven 停止处理并抛出错误(找不到依赖库)。

转自:http://lib.csdn.net/article/architecture/5384

摘要:本部分首先简单介绍 sharding 系统的基本架构,然后重点介绍 sharding 机制中常用的三种表数据划分方法。

一. 数据划分算法

1. Sharding 系统的基本结构

上节我们说到 Sharding 可以简单定义为将大数据库分布到多个物理节点上的一个分区方案。每个 shard 都被放置在一个节点上面。Sharding 系统是一个 shared-nothing 的系统,基本上都采用下图中所示的架构。最下面是很多数据库服务器节点,每个节点上面都会运行一个或多个数据库的实例。中间一层叫做查询路由器,客户端的连接都通过它进行转发。查询路由器负责解析用户的查询语句,并将这些语句转发到包含有所需要的数据的 shard 节点上面去执行。执行的结果也会通过查询路由器进行汇总并发送给相应的客户端。

数据库分片

对于这样一个 sharding 系统,我们需要考虑到下面几个问题:如何将数据划分到多个 shard 节点上面;用户的查询语句如何正确的转发到相应的节点上面去执行;当节点数据变化的时候怎样重新划分数据。对于数据划分和查询路由来说,所用的算法一般是对应的。下面就讲一下一些常用的数据划分的方法。这里的假设前提是:只考虑单个表,并且这个表的划分键(Partitioning Key)已经被指定。

2. 常见的表划分算法

本节介绍三种常见的数据划分方法:轮流放置(Round-Robin)、一致性哈希(Consistent Hashing)和区间划分(Range-based Partitioning)。

2.1 Round-Robin

轮流放置是最简单的划分方法:即每条元组都会被依次放置在下一个节点上,以此进行循环。一般在实际应用中为了处理的方便,通常按照主键的值来决定次序从而进行划分。即给定一个表T,表T的划分键(Partitioning Key)是 k,需要划分的节点数目 N,那么元组 $t ∈ T$ 将会被放置在节点 n 上面,其中 $n = t.k mod N$。由于划分只与划分键有关,因此我们可以把对元组的划分简化为对数字的划分,对于不是数字的键值可以通过其它方式比如哈希转化为数字形式。下面给出一个例子来表示这种划分方式,把 9 个元组分布到 3 个节点上的情况。

轮流放置

但是,简单的直接用划分键上的值来计算放置节点的算法可能会造成数据的不均匀。因此,轮流放置有很多改进版,比如说哈希方式(Hashing),即 $n = hash(t.k) mod N$。先将划分键的值进行 hash 操作,变成一个与输入分布无关、输出均匀的值,然后再进行取模操作。哈希函数可以有很多选择,你可以针对你的应用的特征去选取。

Pros: 轮流放置算法的实现非常简单,而且几乎不需要元数据就可以进行查询的路由,因此有着比较广泛的应用。例如 EMC 的 Greenplum 的分布式数据仓库采用的就是轮流放置和哈希相结合的方式。

Cons: 轮流放置同样具有很明显的缺点:当系统中添加或者删除节点时,数据的迁移量非常巨大。举个有 20 个节点的例子(下表),当系统由 4 个节点变为 5 个节点时,会有如下的放置结果:红色部分是 $mod 4$ 和 $mod 5$ 时结果不相等的情况,不相等意味着这些元组当系统由 4 个节点变为 5 个节点时需要进行迁移。也就是说多达 80% 的元组都需要迁移。数据的迁移会对系统的性能造成很大的影响,严重时可能会中断系统的服务。当系统的节点数目频繁变化时,是不提倡使用这种方式的。

轮流放置扩展

数据迁移量大的问题可以通过改进轮流放置算法来达到,比较常见的两个改进算法是一致性哈希和分区划分算法。

2.2 Consistent Hashing

一致性哈希是一种特殊的哈希方式。传统的哈希方式(在上一小节中讲到)在当节点数目发生变化时,会引起大量的数据迁移。而使用一致性哈希则不会产生这种问题。一致性哈希最早是一个分布式缓存(Distributed Caching)系统的放置算法(现在很热门的Memcached就用的是一致性哈希)。但是现在它已经被广泛应用到了其它各个领域。对于任何一个哈希函数,其输出值都有一个取值范围,我们可以将这个取值区间画成一个环,如下图所示:

一致性哈希

通过哈希函数,每个节点都会被分配到环上的一个位置,每个键值也会被映射到环上的一个位置。这个键值最终被放置在距离该它的位置最近的,且位置编号大于等于该值的节点上面,即放置到顺时针的下一个节点上面。下图形象的表示了这种放置方案,其中 Node 0 上面放置 Range 0 上面的数据,以此类推。

一致性哈希

Pros: 由于采用的哈希函数通常是与输入无关的均匀函数,因此当键值和节点都非常的多的时候,一致性哈希可以达到很好的分布式均匀性。并且由于特殊的放置规则,一致性哈希在节点数据发生变动时可以将影响控制在局部区间内,从而保证非常少的数据迁移(接近理论上的最小值)。当增加一个节点时,只有这个节点所在的区间内的数据需要被重新划分,如下图中,只需要将 range 2 上面的数据会从 node 1 中迁移到 node 3 上面。当删除一个节点时,只需要将这个节点上面的数据迁移到下一个节点上面,比如删除 node 3,只把 range 2 上面的数据迁移到 node 1 上面就可以并,而其它的数据是不需要迁移变动的。

一致性哈希扩展

一致性哈希有非常广泛的应用,Key-Value 系统,文档数据库或者分布式关系数据库都可以使用。Amazon 的 NoSQL 系统 Dynamo 应用采用的就是一种改进的一致性哈希算法。在这个系统中又引入了虚拟节点的概念,使得这个算法的 load balance 更加的好,并且同时考虑了复制技术。环上的点就变成了虚拟节点,然后再采用其它的方式将这些虚拟节点映射到实际的物理节点上面去。这使得系统有很好的可扩展性和可用性。

2.3 Range-Based Partitioning

区间划分是现在很热门的 NoSQL 数据库 MongoDB 的 sharding 方案中所使用的算法。系统会首先把所有的数据划分为多个区间,然后再将这些区间分配到系统的各个节点上面。最简单的区间划分是一个节点只持有一个区间:在有 n 个节点的情况下,将划分键的取值区间均匀划分(这里的均匀是指划分后的每个 partition 的数据量尽量一样大,而并非值域区间一样大)为 n 份,然后每个节点持有一块。例如,按照用户名首字母进行划分,可能有以下的划分方案:

区间划分

如果发生数据分布不均匀的情况,可以通过调整区间分布达到均匀情况,数据迁移同样会很小。

区间划分

但是另外一些情况下,可能会导致连锁迁移。

  • 情况一:数据分布不均,调整导致的连锁迁移

区间划分数据迁移扩展

  • 情况二:增加或删除节点导致的连锁迁移:

区间划分数据迁移扩展2

为了解决这个问题,MongoDB 采用的是每个节点持有多个区间的方案(Multiple range shards)。当需要进行迁移的时候,将持有过多数据的节点上的区间分裂,使得分裂出来的区间刚好满足迁移需要,然后再进行迁移。举例来说(下图),如果 shard 中存有 $[a, f]$ 区间的数据,数据量为 500G,此时需要从 shard 1 上面迁移 100G 到 shard 4,以保证数据的均匀分布。经统计,shard 1 中的 $[a, d]$ 段的数据为 400G,$[d, f]$ 段数据为 100G,因此将 shard 1中的 $[d, f]$ 段的数据直接迁移到 shard 4 上面。同理,需要从 shard 2 中迁移 100G 的数据到 shard 3 中。这种迁移方式的数据迁移量是理论上的最小值。

区间划分数据迁移扩展最优

Cons: 每个节点多个区间的做法的缺点是使得对元数据的处理变得复杂,我们需要记录每个节点上面存储的所有区间。但是一般来说,每个节点上面的区间数目不是很大,因此元数据的数目不会很大。这种同时保证了数据的最小迁移,并且实现也比较简单的方案是一个很理想的做法,虽然它的无数据管理和同步上面会有一些问题。

另外,区间划分非常适合处理有区间查询的查询语句,但是也带来很大的一个 trade-off。如果一个查询需要访问到多条元组,那么对区间的边界的选取就变得非常棘手,如果选择不当的话,很容易造成一个查询需要在多个节点上面进行运行的情况,这种跨节点的操作会对系统的性能进行很大的影响。

转自:http://blog.jobbole.com/89140/

前阵子从支付宝转账1万块钱到余额宝,这是日常生活的一件普通小事,但作为互联网研发人员的职业病,我就思考支付宝扣除1万之后,如果系统挂掉怎么办,这时余额宝账户并没有增加1万,数据就会出现不一致状况了。

上述场景在各个类型的系统中都能找到相似影子,比如在电商系统中,当有用户下单后,除了在订单表插入一条记录外,对应商品表的这个商品数量必须减1吧,怎么保证?!在搜索广告系统中,当用户点击某广告后,除了在点击事件表中增加一条记录外,还得去商家账户表中找到这个商家并扣除广告费吧,怎么保证?!等等,相信大家或多或多少都能碰到相似情景。

本质上问题可以抽象为:当一个表数据更新后,怎么保证另一个表的数据也必须要更新成功。

1. 本地事务

还是以支付宝转账余额宝为例,假设有

  • 支付宝账户表:A(id,userId,amount)
  • 余额宝账户表:B(id,userId,amount)
  • 用户的userId=1;

从支付宝转账1万块钱到余额宝的动作分为两步:

  1. 支付宝表扣除1万:update A set amount=amount-10000 where userId=1;
  2. 余额宝表增加1万:update B set amount=amount+10000 where userId=1;

如何确保支付宝余额宝收支平衡呢?

有人说这个很简单嘛,可以用事务解决。

1
2
3
4
5
Begin transaction
update A set amount=amount-10000 where userId=1;
update B set amount=amount+10000 where userId=1;
End transaction
commit;

非常正确,如果你使用spring的话一个注解就能搞定上述事务功能。

1
2
3
4
5
@Transactional(rollbackFor=Exception.class)
public void update() {
updateATable(); //更新A表
updateBTable(); //更新B表
}

如果系统规模较小,数据表都在一个数据库实例上,上述本地事务方式可以很好地运行,但是如果系统规模较大,比如支付宝账户表和余额宝账户表显然不会在同一个数据库实例上,他们往往分布在不同的物理节点上,这时本地事务已经失去用武之地。

既然本地事务失效,分布式事务自然就登上舞台。

2. 分布式事务—两阶段提交协议

两阶段提交协议(Two-phase Commit,2PC)经常被用来实现分布式事务。一般分为协调器C和若干事务执行者Si两种角色,这里的事务执行者就是具体的数据库,协调器可以和事务执行器在一台机器上。

两阶段提交

  1. 我们的应用程序(client)发起一个开始请求到TC;

  2. TC先将消息写到本地日志,之后向所有的Si发起消息。以支付宝转账到余额宝为例,TC给A的prepare消息是通知支付宝数据库相应账目扣款1万,TC给B的prepare消息是通知余额宝数据库相应账目增加1w。为什么在执行任务前需要先写本地日志,主要是为了故障后恢复用,本地日志起到现实生活中凭证 的效果,如果没有本地日志(凭证),出问题容易死无对证;

  3. Si收到消息后,执行具体本机事务,但不会进行commit,如果成功返回,不成功返回。同理,返回前都应把要返回的消息写到日志里,当作凭证。

  4. TC收集所有执行器返回的消息,如果所有执行器都返回yes,那么给所有执行器发生送commit消息,执行器收到commit后执行本地事务的commit操作;如果有任一个执行器返回no,那么给所有执行器发送abort消息,执行器收到abort消息后执行事务abort操作。

注:TC或Si把发送或接收到的消息先写到日志里,主要是为了故障后恢复用。如某一Si从故障中恢复后,先检查本机的日志,如果已收到,则提交,如果则回滚。如果是,则再向TC询问一下,确定下一步。如果什么都没有,则很可能在阶段Si就崩溃了,因此需要回滚。

现如今实现基于两阶段提交的分布式事务也没那么困难了,如果使用java,那么可以使用开源软件atomikos(http://www.atomikos.com/)来快速实现。

不过但凡使用过的上述两阶段提交的同学都可以发现性能实在是太差,根本不适合高并发的系统。为什么?

  1. 两阶段提交涉及多次节点间的网络通信,通信时间太长!
  2. 事务时间相对于变长了,锁定的资源的时间也变长了,造成资源等待时间也增加好多!
    正是由于分布式事务存在很严重的性能问题,大部分高并发服务都在避免使用,往往通过其他途径来解决数据一致性问题。

3. 使用消息队列来避免分布式事务

如果仔细观察生活的话,生活的很多场景已经给了我们提示。

比如在北京很有名的姚记炒肝点了炒肝并付了钱后,他们并不会直接把你点的炒肝给你,而是给你一张小票,然后让你拿着小票到出货区排队去取。为什么他们要将付钱和取货两个动作分开呢?原因很多,其中一个很重要的原因是为了使他们接待能力增强(并发量更高)。

还是回到我们的问题,只要这张小票在,你最终是能拿到炒肝的。同理转账服务也是如此,当支付宝账户扣除1万后,我们只要生成一个凭证(消息)即可,这个凭证(消息)上写着“让余额宝账户增加 1万”,只要这个凭证(消息)能可靠保存,我们最终是可以拿着这个凭证(消息)让余额宝账户增加1万的,即我们能依靠这个凭证(消息)完成最终一致性。

3.1 如何可靠保存凭证(消息)

有两种方法:

3.1.1 业务与消息耦合的方式

支付宝在完成扣款的同时,同时记录消息数据,这个消息数据与业务数据保存在同一数据库实例里(消息记录表表名为message)。

1
2
3
4
5
Begin transaction
update A set amount=amount-10000 where userId=1;
insert into message(userId, amount,status) values(1, 10000, 1);
End transaction
commit;

上述事务能保证只要支付宝账户里被扣了钱,消息一定能保存下来。

当上述事务提交成功后,我们通过实时消息服务将此消息通知余额宝,余额宝处理成功后发送回复成功消息,支付宝收到回复后删除该条消息数据。

3.1.2 业务与消息解耦方式

上述保存消息的方式使得消息数据和业务数据紧耦合在一起,从架构上看不够优雅,而且容易诱发其他问题。为了解耦,可以采用以下方式。

  1. 支付宝在扣款事务提交之前,向实时消息服务请求发送消息,实时消息服务只记录消息数据,而不真正发送,只有消息发送成功后才会提交事务;

  2. 当支付宝扣款事务被提交成功后,向实时消息服务确认发送。只有在得到确认发送指令后,实时消息服务才真正发送该消息;

  3. 当支付宝扣款事务提交失败回滚后,向实时消息服务取消发送。在得到取消发送指令后,该消息将不会被发送;

  4. 对于那些未确认的消息或者取消的消息,需要有一个消息状态确认系统定时去支付宝系统查询这个消息的状态并进行更新。为什么需要这一步骤,举个例子:假设在第2步支付宝扣款事务被成功提交后,系统挂了,此时消息状态并未被更新为“确认发送”,从而导致消息不能被发送。

  • 优点:消息数据独立存储,降低业务系统与消息系统间的耦合;

  • 缺点:一次消息发送需要两次请求;业务处理服务需要实现消息状态回查接口。

3.2 如何解决消息重复投递的问题

还有一个很严重的问题就是消息重复投递,以我们支付宝转账到余额宝为例,如果相同的消息被重复投递两次,那么我们余额宝账户将会增加2万而不是1万了。

为什么相同的消息会被重复投递?比如余额宝处理完消息msg后,发送了处理成功的消息给支付宝,正常情况下支付宝应该要删除消息msg,但如果支付宝这时候悲剧的挂了,重启后一看消息msg还在,就会继续发送消息msg。

解决方法很简单,在余额宝这边增加消息应用状态表(message_apply),通俗来说就是个账本,用于记录消息的消费情况,每次来一个消息,在真正执行之前,先去消息应用状态表中查询一遍,如果找到说明是重复消息,丢弃即可,如果没找到才执行,同时插入到消息应用状态表(同一事务)。

1
2
3
4
5
6
7
8
for each msg in queue
Begin transaction
select count(*) as cnt from message_apply where msg_id=msg.msg_id;
if cnt==0 then
update B set amount=amount+10000 where userId=1;
insert into message_apply(msg_id) values(msg.msg_id);
End transaction
commit;

ebay的研发人员其实在2008年就提出了应用消息状态确认表来解决消息重复投递的问题:http://queue.acm.org/detail.cfm?id=1394128。

两阶段提交

分布式事务,常见的两个处理办法就是两段式提交和补偿。

两段式提交典型的就是XA,有个事务协调器,告诉大家,来都准备好提交,大家回复,都准备好了,然后协调器告诉大家,一起提交,大家都提交了。

补偿比较好理解,先处理业务,然后定时或者回调里,检查状态是不是一致的,如果不一致采用某个策略,强制状态到某个结束状态(一般是失败状态),然后就世界太平了。典型的就是冲正操作。