20191220 使用Redis實現樂觀鎖

鎖機制:

樂觀鎖:1)通過版本號來實現,先查詢獲取版本號,在更新的時候校驗版本號並修改。

悲觀鎖:同步關鍵字就是悲觀鎖,也稱爲排它鎖。

樂觀鎖還讓用戶查詢當前版本號,悲觀鎖如果不釋放,查都不讓查詢。

樂觀鎖存在多種實現方式:mysql數據庫版本號,redis實現,CAS實現等。

在併發情況下,使用鎖機制,防止爭搶資源。

 

悲觀鎖是對數據的修改持悲觀態度(認爲數據在被修改的時候一定會存在併發問題),因此在整個數據處理過程中將數據鎖定。悲觀鎖的實現,往往依靠數據庫提供的鎖機制(也只有數據庫層提供的鎖機制才能真正保證數據訪問的排他性,否則,即使在應用層中實現了加鎖機制,也無法保證外部系統不會修改數據)

 

鎖機制是爲了解決高併發問題。

使用悲觀鎖的原理就是,當我們在查詢出goods信息後就把當前的數據鎖定,直到我們修改完畢後再解鎖。

要使用悲觀鎖,我們必須關閉mysql數據庫的自動提交屬性。

set autocommit=0;  

關閉了mysql的autocommit,所以需要手動控制事務的提交。

使用select…for update會把數據給鎖住,不過我們需要注意一些鎖的級別,MySQL InnoDB默認Row-Level Lock,所以只有「明確」地指定主鍵,MySQL 纔會執行Row lock (只鎖住被選取的數據) ,否則MySQL 將會執行Table Lock (將整個數據表單給鎖住)。

如果無主鍵或者主鍵不明確,會鎖住整個表。


-- 商品表
CREATE TABLE t_goods (
id int not null AUTO_INCREMENT comment 'id信息',
name VARCHAR(20) comment '商品名稱',
status int comment '商品狀態',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='商品表';

-- 訂單表
CREATE TABLE t_orders (
id int not null AUTO_INCREMENT comment 'id信息',
goods_id int comment '商品id',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='訂單表';

insert into t_goods(name,status) values('無極書',1)
insert into t_orders(name,status) values('太極',1)


-- 在高併發環境下,這三行代碼可能存在問題
select status from t_goods where id=1;
insert into t_orders(goods_id) values(1);
update t_goods set status=2 where id=1;

-- 關閉事務
set autocommit=0;
start TRANSACTION;
select status from t_goods where id=1 for update;
insert into t_orders(goods_id) values(1);
update t_goods set status=2 where id=1;
commit;

set autocommit =0;
select status from t_goods where id=1 for update;
commit;

set autocommit=0;
SELECT * from t_goods where id=4 for update;

set autocommit=0;
SELECT * from t_goods where status=1 for update;
commit;

set autocommit=0;
SELECT * from t_goods where id>1 for update;

沒有提交事務導致行鎖住 還是因爲select…for update 鎖住數據?

不是因爲事務,而是因爲select…for update 鎖住數據。

 

樂觀鎖,使用版本標識來確定讀到的數據與提交時的數據是否一致。提交後修改版本標識,不一致時可以採取丟棄和再次嘗試的策略。

 

1、悲觀鎖,前提是,一定會有併發搶佔資源,強行獨佔資源,在整個數據處理過程中,將數據處於鎖定狀態。獨佔鎖其實就是一種悲觀鎖,排它鎖。

2、樂觀鎖,前提是,不會發生併發搶佔資源,只有在提交操作的時候檢查是否違反數據完整性。只能防止髒讀後數據的提交,不能解決髒讀。

 

Java裏面進行多線程通信的主要方式就是共享內存的方式,共享內存主要的關注點有兩個:可見性和有序性。加上覆合操作的原子性,我們可以認爲Java的線程安全性問題主要關注點有3個:可見性、有序性和原子性。

Java內存模型(JMM)解決了可見性和有序性的問題,而鎖解決了原子性的問題。

 

基於redis的樂觀鎖實踐

Redis的事務機制以及watch指令(CAS)實現樂觀鎖。

所謂樂觀鎖,就是利用版本號比較機制,只是在讀數據的時候,將讀到的數據的版本號一起讀出來,當對數據的操作結束後,準備寫數據的時候,再進行一次數據版本號的比較,若版本號沒有變化,即認爲數據是一致的,沒有更改,可以直接寫入,若版本號有變化,則認爲數據被更新,不能寫入,防止髒寫。

 

基於redis實現樂觀鎖。

redis的事務,涉及到的指令,主要有multi,exec,discard。而實現樂觀鎖的指令,在事務基礎上,主要是watch指令.

multi和exec之間的指令。 鍵值對變化時,指令不執行。

利用watch指令,基於CAS機制,簡單的樂觀鎖。

watch指令在一次事務執行完畢後,即結束其生命週期。

基於redis的樂觀鎖,可以得出一個結論:

1. 樂觀鎖的實現,必須基於WATCH,然後利用redis的事務。

2. WATCH生命週期,只是和事務關聯的,一個事務執行完畢(執行了exec命令),相應的watch的生命週期即結束。

測試redis樂觀鎖機制,需要開啓兩個窗口。

incr 命令:對數值+1;

decr 命令:對數值-1;

Redis Decrby 命令將 key 所儲存的值減去指定的減量值。 decrby key 20

 

Redis實現樂觀鎖比較簡單,主要思路就是watch一個key的變動,並在watch和unwatch之間做一個類似事務操作,只有當事務操作成功,整體循環纔會跳出,當然,當操作期間watch的key變動時候,提交事務操作時候,事務操作將會被取消。

public void testRedisSyn(int clientName,String clientList) {

        //redis中存儲商品數量爲(goodsNum:100)
        String key = "goodsNum";
        Jedis jedis = new Jedis("192.168.140.98", 6379);
        jedis.auth("redis密碼");

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        while (true) {
            try {
                jedis.watch(key);
                System.out.println("顧客:" + clientName + "開始搶商品");
                System.out.println("當前商品的個數:" + jedis.get(key));
                //當前商品個數
                int prdNum = Integer.parseInt(jedis.get(key));
                if (prdNum > 0) {

                    //開啓事務,返回一個事務控制對象
                    Transaction transaction = jedis.multi();
                    //預先在事務對象中裝入要執行的操作
                    transaction.set(key, String.valueOf(prdNum - 1));
                    List<Object> exec = transaction.exec();
                    if (exec == null || exec.isEmpty()) {
                        //可能是watch-key被外部修改,或者是數據操作被駁回
                        System.out.println("悲劇了,顧客:" + clientName + "沒有搶到商品");
                    } else {
                        //這個命令是做啥的。//搶到商品記錄一下
                        jedis.sadd(clientList, clientName+"");
                        System.out.println("好高興,顧客:" + clientName + "搶到商品");
                        break;
                    }
                }
            } catch (NumberFormatException e) {
                e.printStackTrace();
            }finally {
                jedis.unwatch();
            }
        }

    }

1.在不成功的情況下,一般需要重試幾次,在重試的過程中每次循環都需要重新watch操作,因爲每次事務提交之後,watch操作都會失效。

2.在事務提交之後返回的結果對象分爲幾種情況

1)事務提交前,watch的key發生改變,返回的List對象並不是null,而是一個初始化後的空對象(size==0)

2)事務提交前,watch的key沒有改變,事務提交成功,返回的List對象中有一個"OK"的String對象。

 

如果是高併發場景,就使用樂觀鎖,因爲樂觀鎖性能比悲觀鎖好;悲觀鎖不適合高併發場景。

樂觀鎖的實現方式:數據庫,redis;版本號。

樂觀鎖的使用場景,悲觀鎖的使用場景。

synchronized是悲觀鎖,這種線程一旦得到鎖,其他需要鎖的線程就掛起的情況就是悲觀鎖。

CAS操作的就是樂觀鎖,每次不加鎖而是假設沒有衝突而去完成某項操作,如果因爲衝突失敗就重試,直到成功爲止。

 

1、查詢時 獲取樂觀鎖的標記

2、只有更新的時候,纔會更新鎖

3、重試,重新獲取鎖,然後去更新。

鎖機制:樂觀鎖(版本號,CAS),悲觀鎖(同步鎖)

樂觀鎖的實現,以及樂觀鎖的使用場景。 使用DB實現樂觀鎖。

使用樂觀鎖進行下單減庫存的操作。

4、watch,監視鍵值對,作用時如果事務提交exec時發現監視的監視對發生變化,事務將被取消。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章