JPA使用乐观锁应对高并发
高并发系统的挑战
在部署分布式系统时,我们通常把多个微服务部署在内网集群中,再用API网关聚合起来对外提供。为了做负载均衡,通常会对每个微服务都启动多个运行实例,通过注册中心去调用。
那么问题来了,因为有多个实例运行都是同一个应用,虽然微服务网关会把每一个请求只转发给一个实例,但当面对高并发时,但它们仍然可能同时操作同一个数据库表,这会不会引发什么问题呢?
悲观锁的问题
比如电商中常见的商品秒杀系统,在用户抢购商品过程中,会有大量并发请求,很可能同时读写一个包含商品剩余数量的表,这种一般要给数据库加锁,否则很容易出现商品超卖错卖的情况。
如果使用数据库自带的锁机制,也就是悲观锁,在写入的时候锁定数据库,其他修改请求到来时就必须等待锁释放,但这就使效率降下来了,而且高并发场景下可能有的请求一直抢不到锁,就会长时间卡在那导致请求失败,然后有大量重试,系统可能会发生连接数耗尽等异常。
乐观锁是个好东西
查阅资料发现乐观锁是个好东西,它是为数据库表增加一个标识数据版本的version字段来实现的,读取数据时把version字段一同读出,写入数据库时比对version字段就知道数据是否被更改过,如果version不相等就说明持有的是过期数据,不能写入,如果相等就可以写入,并把version加一。
乐观锁在写入数据库的时候,才会检查数据是否冲突,如果发现冲突了,就放弃写入,返回写入失败的信息,相比于悲观锁,这是一种轻量级的对数据的锁定方式,能够应对高并发需求。
给数据库添加乐观锁
说乐观锁是个好东西,首先得说 JPA 是个好东西,因为Spring Data JPA已经内置了乐观锁的实现,给数据库表添加乐观锁很简单,添加一个整型字段,并加入@Version注解就可以了,每次提交数据时JPA会自动检查版本。
1
2
3
4
5
6
7
8
|
@Entity @Table (name = "m_order" ) public class Order { ... @Version private int version; ... } |
乐观锁 -业务判断 解决高并发
在解决高并发问题时,如果是分布式系统显然我们只能够使用数据库端加锁机制来解决这个问题,但是这种同步机制或者数据库物理锁机制会牺牲一部分的性能,所以常常以另外一种方式来解决这个问题 就是乐观锁模式
银行两操作员同时操作同一账户就是典型的乐观锁模式。
比如A、B操作员同时读取一余额为1000元的账户,A操作员为该账户增加100元,B操作员同时为该账户扣除50元,A先提交,B后提交。最后实际账户余额为1000-50=950元,但本该为1000+100-50=1050。这就是典型的并发问题。
乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本(Version)记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。
读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。
对于上面修改用户帐户信息的例子而言,假设数据库中帐户信息表中有一个version字段,当前值为1;而当前帐户余额字段(balance)为1000元。假设操作员A先更新完,操作员B后更新。
- a、操作员A此时将其读出(version=1),并从其帐户余额中增加100(1000+100=1100)。
- b、在操作员A操作的过程中,操作员B也读入此用户信息(version=1),并从其帐户余额中扣除50(1000-50=950)。
- c、操作员A完成了修改工作,将数据版本号加一(version=2),连同帐户增加后余额(balance=1100),提交至数据库更新,此时由于提交数据版本大于数据库记录当前版本,数据被更新,数据库记录version更新为2。
- d、操作员B完成了操作,也将版本号加一(version=2)试图向数据库提交数据(balance=950),但此时比对数据库记录版本时发现,操作员B提交的数据版本号为2,数据库记录当前版本也为2,不满足 “提交版本必须大于记录当前版本才能执行更新 “的乐观锁策略,因此,操作员B的提交被驳回。
- 这样,就避免了操作员B用基于version=1的旧数据修改的结果覆盖操作员A的操作结果的可能。
操作员A操作如下:
1
2
3
4
5
6
7
|
select id, balance, version from account where id= "1" ; 查询结果:id=1, balance=1000, version=1 update account set balance=balance+100, version=version+1 where id= "1" and version=1 select id, balance, version from account where id= "1" ; 查询结果:id=1, balance=1100, version=2 |
操作员B操作如下:
1
2
3
4
5
6
7
8
|
select id, balance, version from account where id= "1" ; 查询结果:id=1, balance=1000, version=1 #操作员A已修改成功,实际account.balance=1100、account.version=2,操作员B也将版本号加一(version=2)试图向数据库提交数据(balance=950),但此时比对数据库记录版本时发现,操作员B提交的数据版本号为2,数据库记录当前版本也为2,不满足 “提交版本必须大于记录当前版本才能执行更新 “的乐观锁策略,因此,操作员B的提交被驳回。 update account set balance=balance-50, version=version+1 where id= "1" and version=1 select id, balance, version from account where id= "1" ; 查询结果:id=1, balance=1100, version=2 |
Hibernate、JPA等ORM框架或者实现,是使用版本号,再判断UPDATE后返回的数值
以上为个人经验,希望能给大家一个参考,也希望大家多多支持服务器之家。
原文链接:https://blog.csdn.net/ta_ab/article/details/77920510