背景
在电商购物的场景下,当咱们点击购物时,后端服务就会对相应的商品进行减库存操做。在单实例部署的状况,咱们能够简单地使用JVM提供的锁机制对减库存操做进行加锁,防止多个用户同时点击购买后致使的库存不一致问题。java
但在实践中,为了提升系统的可用性,咱们通常都会进行多实例部署。而不一样实例有各自的JVM,被负载均衡到不一样实例上的用户请求不能经过JVM的锁机制实现互斥。sql
所以,为了保证在分布式场景下的数据一致性,咱们通常有两种实践方式:1、使用MySQL乐观锁;2、使用分布式锁。数据库
本文主要介绍MySQL乐观锁,关于分布式锁我在下一篇博客中介绍。后端
乐观锁简介
乐观锁(Optimistic Locking)与悲观锁相对应,咱们在使用乐观锁时会假设数据在极大多数状况下不会造成冲突,所以只有在数据提交的时候,才会对数据是否产生冲突进行检验。若是产生数据冲突了,则返回错误信息,进行相应的处理。并发
那咱们如何来实现乐观锁呢?通常采用如下方式:使用版本号(version)机制来实现,这是乐观锁最经常使用的实现方式。app
版本号
那什么是版本号呢?版本号就是为数据添加一个版本标志,一般我会为数据库中的表添加一个int类型的"version"字段。当咱们将数据读出时,咱们会将version字段一并读出;当数据进行更新时,会对这条数据的version值加1。当咱们提交数据的时候,会判断数据库中的当前版本号和第一次取数据时的版本号是否一致,若是两个版本号相等,则更新,不然就认为数据过时,返回错误信息。咱们能够用下图来讲明问题:负载均衡
如图所示,若是更新操做如第一个图中同样顺序执行,则数据的版本号会依次递增,不会有冲突出现。可是像第二个图中同样,不一样的用户操做读取到数据的同一个版本,再分别对数据进行更新操做,则用户的A的更新操做能够成功,用户B更新时,数据的版本号已经变化,因此更新失败。dom
代码实践
咱们对某个商品减库存时,具体操做分为如下3个步骤:分布式
-
查询出商品的具体信息ide
-
根据具体的减库存数量,生成相应的更新对象
-
修改商品的库存数量
为了使用MySQL的乐观锁,咱们须要为商品表goods加一个版本号字段version,具体的表结构以下:
1
2
3
4
5
6
7
|
CREATE TABLE `goods` (
`id`
int
(
11
) NOT NULL AUTO_INCREMENT,
`name` varchar(
64
) NOT NULL DEFAULT
''
,
`remaining_number`
int
(
11
) NOT NULL,
`version`
int
(
11
) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=
2
DEFAULT CHARSET=utf8;
|
Goods类的Java代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
* 商品名字
*/
private
String name;
/**
* 库存数量
*/
private
Integer remainingNumber;
/**
* 版本号
*/
private
Integer version;
@Override
public
String toString() {
return
"Goods{"
+
"id="
+ id +
", name='"
+ name + '\
''
+
", remainingNumber="
+ remainingNumber +
", version="
+ version +
'}'
;
}
}
|
GoodsMapper.java:
1
2
3
4
5
|
public
interface
GoodsMapper {
Integer updateGoodCAS(Goods good);
}
|
GoodsMapper.xml以下:
1
2
3
4
5
6
7
8
9
|
<update id=
"updateGoodCAS"
parameterType=
"com.ztl.domain.Goods"
>
<![CDATA[
update goods
set `name`=#{name},
remaining_number=#{remainingNumber},
version=version+
1
where id=#{id} and version=#{version}
]]>
</update>
|
GoodsService.java 接口以下:
1
2
3
4
5
|
public
interface
GoodsService {
@Transactional
Boolean updateGoodCAS(Integer id, Integer decreaseNum);
}
|
GoodsServiceImpl.java类以下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
@Service
public
class
GoodsServiceImpl
implements
GoodsService {
@Autowired
private
GoodsMapper goodsMapper;
@Override
public
Boolean updateGoodCAS(Integer id, Integer decreaseNum) {
Goods good = goodsMapper.selectGoodById(id);
System.out.println(good);
try
{
Thread.sleep(
3000
);
//模拟并发状况,不一样的用户读取到同一个数据版本
}
catch
(InterruptedException e) {
e.printStackTrace();
}
good.setRemainingNumber(good.getRemainingNumber() - decreaseNum);
int
result = goodsMapper.updateGoodCAS(good);
System.out.println(result ==
1
?
"success"
:
"fail"
);
return
result ==
1
;
}
}
|
GoodsServiceImplTest.java测试类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
@RunWith
(SpringRunner.
class
)
@SpringBootTest
public
class
GoodsServiceImplTest {
@Autowired
private
GoodsService goodsService;
@Test
public
void
updateGoodCASTest() {
final
Integer id =
1
;
Thread thread =
new
Thread(
new
Runnable() {
@Override
public
void
run() {
goodsService.updateGoodCAS(id,
1
);
//用户1的请求
}
});
thread.start();
goodsService.updateGoodCAS(id,
2
);
//用户2的请求
System.out.println(goodsService.selectGoodById(id));
}
}
|
输出结果:
1
2
3
4
5
|
Goods{id=
1
, name=
'手机'
, remainingNumber=
10
, version=
9
}
Goods{id=
1
, name=
'手机'
, remainingNumber=
10
, version=
9
}
success
fail
Goods{id=
1
, name=
'手机'
, remainingNumber=
8
, version=
10
}
|
代码说明:
在updateGoodCASTest()的测试方法中,用户1和用户2同时查出id=1的商品的同一个版本信息,而后分别对商品进行库存减1和减2的操做。从输出的结果能够看出用户2的减库存操做成功了,商品库存成功减去2;而用户1提交减库存操做时,数据版本号已经改变,因此数据变动失败。
这样,咱们就能够经过MySQL的乐观锁机制保证在分布式场景下的数据一致性。
以上。