解決"併發下查詢並更新帶來的問題"

場景:

在平常開發中常常遇到先根據條件判斷某條數據是否存在,若是不存在的話就插入,若是存在的話就更新或提示異常。通常代碼的模式都寫成下面這個樣子,是一種很常見的寫法,可是在併發狀況下很容易會重複插入兩條數據,大概的狀況就是第一個請求進來,沒有查詢到該用戶經過了if判斷,可是if中有比較耗時的邏輯,在第一個請求還沒執行insert的時候第二個請求也進來了,由於這個時候第一個請求還沒執行insert操做,因此第二個請求也沒有查詢到該用戶也經過了if判斷,這個樣子就形成了兩條重複的數據。redis

// 查詢名字叫user1的用戶是否存在
UserVo userVo= userMapper.selectUserByName("user1");
    // 若是不存在就插入數據
    if (userVo==null) {
        Thread.sleep(10000);
        UserVo userVo = new UserVo();
        userVo.setUserName("user1");
        userMapper.insert(userVo);
     }
}

解決方法:

1.使用synchronized同步代碼塊

直接將查詢校驗邏輯和插入邏輯都進行同步,也就是說第一個請求的邏輯沒結束,第二個請求就會一直等待着,只有當第一個請求執行完同步代碼塊中的邏輯釋放鎖後第二個請求才能獲取到鎖執行這段邏輯。spring

private Object obj = new Object();

synchronized (object){
    // 查詢名字叫user1的用戶是否存在
    UserVo userVo= userMapper.selectUserByName("user1");
    // 若是不存在就插入數據
    if (userVo==null) {
        Thread.sleep(10000);
        UserVo userVo = new UserVo();
        userVo.setUserName("user1");
        userMapper.insert(userVo);
     }
}

2.使用Lock

其實和synchronized代碼塊是相同的做用,可是要注意必須在finally中釋放鎖,避免出現異常死鎖了。數據庫

private Lock lock = new ReentrantLock();
try {
    lock.lock();
    // 查詢名字叫user1的用戶是否存在
    UserVo userVo = userMapper.selectUserByName("user1");
    // 若是不存在就插入數據
    if (userVo == null) {
        Thread.sleep(10000);
        UserVo userVo = new UserVo();
        userVo.setUserName("user1");
        userMapper.insert(userVo);
    }
} finally {
    lock.unlock();
}

3.給數據庫索引

既然是要根據用戶名字判斷是否有重複數據,因此能夠直接在數據庫上給userName字段添加UNIQUE索引,這樣在第二次重複插入的時候就會提示異常。若是不想重複插入的時候有報錯提示可使用INSERT IGNORE INTO語句。而代碼則沒必要作任何邏輯操做。併發

// 查詢名字叫user1的用戶是否存在
UserVo userVo= userMapper.selectUserByName("user1");
// 若是不存在就插入數據
if (userVo==null) {
    Thread.sleep(10000);
    UserVo userVo = new UserVo();
    userVo.setUserName("user1");
    userMapper.insert(userVo);
   }
}

4.使用redissetnx來做爲鎖

redissetnx命令是隻有當你存入的key不存在時纔會成功存入,並返回1,而若是key已經存在的時候則存入失敗並返回0,咱們能夠拿這個特性來當作鎖。首先這個方法進來第一步就是執行setnx操做,把查詢的用戶名存入redis,而後查詢該用戶是否存在,第一個請求進到if判斷中可是沒執行插入邏輯,第二個請求雖然也沒有查詢到該用戶,可是它的setnx會失敗,由於第一個請求存的key還沒刪除,因此這樣就避免了併發從新插入的問題,並且最大的優勢就是它不像synchronizedLock不管全部請求進來都只能一個一個經過,使用這種方法是隻有當操做同一個用戶有併發請求的時候纔會阻塞,而若是是請求兩個不一樣的用戶時是不會阻塞的,均可以順利經過,由於存入的key是不一樣的。app

// 自動注入spring的redis操做類
@Autowired
private RedisTemplate redisTemplate;

public String addUser (String userName) {
    // 執行setnx命令,存入當前拿來判斷的用戶名
    BoundValueOperations operations = redisTemplate.boundValueOps(userName);
    // 執行setnx命令的結果,這裏封裝的方法是直接返回true和false
    boolean addFlag = operations.setIfAbsent(1);

    // 返回結果
    String result = null;
    
    UserVo userVo= userMapper.selectUserByName(userName);
    try {
        if (userVo == null && addFlag == true) {
            Thread.sleep(10000);
            UserVo userVo = new UserVo();
            userVo.setUserName("user1");
            userMapper.insert(userVo);
            result = "更新成功";
        } else{
            result = "更新失敗";
        }
    } finally {
        // 不管更新成功和失敗都去刪除setnx添加的key
        operations.getOperations().delete(userName);
    }
    return result;
}

總結:

上述四種方法,給數據庫加索引、Lockredis都有使用過,synchronizedLock也差很少,我的感受給數據庫加索引來控制這種併發太死板了,萬一系統中有其餘地方的邏輯是須要重複添加這個字段的數據,這個時候就沒辦法使用索引了,synchronizedLock效率過低了,若是是併發量太大的這種方式確定是不可缺的,而redis的這種方法則效率高不少,比較適合併發量高的操做。code

結尾:

由於本人接觸的系統的併發量也不是很大,因此對這方面的技術也是本身在鑽研摸索,可能會有不少地方有遺漏和錯誤,若是你們有更好的方法歡迎一塊兒留言討論,也歡迎指出錯誤。索引

相關文章
相關標籤/搜索