背景
在電商購物的場景下,當咱們點擊購物時,後端服務就會對相應的商品進行減庫存操做。在單實例部署的狀況,咱們能夠簡單地使用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的樂觀鎖機制保證在分佈式場景下的數據一致性。
以上。