1. 程式人生 > >Redis基本使用二(事務與鎖)

Redis基本使用二(事務與鎖)

開發環境:

  1. JDK11
  2. Redis3.2

Redis事務機制:

與傳統的關係型資料庫類似,NoSQL也存在許多併發訪問的情況,因此出現瞭如何保證資料一致性的問題,處理的方式有很多。
針對不同的業務層次有不同的解決方案:

  1. 檢視層:前端來保證資料一致性,筆者對前端技術熟悉程度還不足以搞定,暫不討論;
  2. 業務層:可以使用執行緒同步來保證資料一致性;
  3. 持久層:在持久層解決資料一致性問題是最優的選擇,此時有悲觀鎖、樂觀鎖等解決方案;

對於Redis而言,可以通過事務與鎖保證資料一致性,首先了解下Redis事務,與關係性資料庫流程類似,程式碼如下:
 

    /**
     * Jedis實現事務
     * @return
     */
    @GetMapping("test6")
    public Object test6()
    {
        String key = "key_1";
        Jedis jedis = new Jedis("127.0.0.1", 6379);
        // 開啟事務
        Transaction transaction = jedis.multi();
        transaction.hset(key, "a", "新恆結衣");
        transaction.hset(key, "b", "hello");
        // 提交事務
        List<Object> list = transaction.exec();
        return list;
    }

    /**
     * redisTemplate包裝類實現事務
     * @return
     */
    @GetMapping("test7")
    public Object test7()
    {
        SessionCallback<String> callback = new SessionCallback<>()
        {
            @Override
            public String execute(RedisOperations operations) throws DataAccessException
            {
                operations.multi();
                operations.opsForValue().set("key_1", "hello");
                System.out.println(1 / 0);
                operations.opsForValue().set("key_2", "world");
                operations.exec();
                return "ok";
            }
        };
        Object result = null;
        try
        {
            result = redisTemplate.execute(callback);
        } catch (Exception e)
        {
            System.out.println("發生異常");
        }
        System.out.println("value1=" + redisTemplate.opsForValue().get("key_1"));
        System.out.println("value2=" + redisTemplate.opsForValue().get("key_2"));
        return result == null ? "result為空" : result;
    }

如上程式碼展示了Redis實現事務的兩種方式,重點看test7方法,事務中出現1/0的異常,而最後執行結果為返回result為空,value1=hello,value2=null,可以發現即使發生異常,事務也會提交,但異常後的資料定義語句不會執行且事務執行結果返回null。

 

Redis鎖機制:

與傳統關係型資料庫類似,事務操作並不能保證資料的一致性,原因也特別簡單,併發量比較大的時候同一個Redis伺服器可能在執行多個事務,以搶購商品為例,假設庫存var=100,對於客戶端來說,每次取出var都需要一次判斷,看var是否大於0,如果是則執行購買業務,反之則返回搶購完畢,以var=1為例,此時3個客戶端幾乎同時讀取var,則均返回1,那麼這三個客戶端都會執行購買業務,造成var=-2的超發情況,程式示例如下:

    public static void main(String[] args)
    {
        var context = new AnnotationConfigApplicationContext(RedisConfig.class);

        Jedis jedis = new Jedis("127.0.0.1", 6379);
        // 初始庫存100
        jedis.set("var", "100");
        test1();
        try
        {
            // 等待測試方法執行完畢
            Thread.sleep(5000);
        }catch (Exception e)
        {
            e.printStackTrace();
        }
        System.out.println("test方法執行完畢,當前var="+jedis.get("var")); 
    }
    
    /**
     * 購買測試
     */
    private static void test1()
    {
        // 模擬200次購買請求
        for (int i = 0; i < 200; i++)
        {
            new Thread(() ->
            {
                Jedis jedis = new Jedis("127.0.0.1", 6379);
                if (Integer.parseInt(jedis.get("var")) <= 0)
                {
                    System.out.println("var已經不足");
                }else
                {
                    Transaction transaction = jedis.multi();
                    transaction.incrBy("var", -1);
                    System.out.println("成功購買");
                    transaction.exec();
                }
            }).start();
        }
    }

最後程式打印出var=-20(具體列印結果具有不確定性)。

接下來使用Redis的鎖機制解決超發問題,程式如下:

    /**
     *  應用鎖機制的購買測試
     */
    private static void test2()
    {
        // 模擬200次購買請求
        for (int i = 0; i < 200; i++)
        {
            new Thread(() ->
            {
                Jedis jedis = new Jedis("127.0.0.1", 6379);
                // 監控key
                jedis.watch("var");
                if (Integer.parseInt(jedis.get("var")) <= 0)
                {
                    System.out.println("var已經不足");
                    return;
                } else
                {
                    Transaction transaction = jedis.multi();
                    transaction.incrBy("var", -1);
                    List list = transaction.exec();
                    if (list.size() == 0)
                    {
                        System.out.println("事務被取消,重新購買");
                        again(jedis);
                    } else
                    {
                        System.out.println("成功購買,list=" + list);
                    }
                }
            }).start();
        }
    }

    /**
     * 重新購買方法
     * @param jedis
     */
    private static void again(Jedis jedis)
    {
        jedis.watch("var");
        if (Integer.parseInt(jedis.get("var")) <= 0)
        {
            System.out.println("var已經不足");
            return;
        }
        Transaction transaction = jedis.multi();
        transaction.incrBy("var", -1);
        List list = transaction.exec();
        if (list.size() == 0)
        {
            System.out.println("事務被取消,重新購買");
            again(jedis);
        } else
        {
            System.out.println("成功購買,list=" + list);
        }
    }

可能有些小夥伴會問為什麼需要再寫一個重新購買方法,原因是固定的迴圈200次中會有大量的購買失敗(watch監控值改變引起的),如果不加重新購買方法會導致最後var值大於0,也就是明明已經200人搶購完畢,但還是有商品沒賣出去。

Redis的鎖機制是由watch命令控制的,它在事務開啟之前去監控一個或者多個key,在所有事務命令入隊執行前一刻會去檢視該key是否被其他客戶端修改過(注意是其他客戶端,自身不算),如果沒有則正常執行,如果有則事務回滾,返回list長度為0。

Redis鎖機制應用流程:

  1. 定義Jedis物件;
  2. watch命令繫結監控的key;
  3. 開啟事務,設定執行命令;
  4. 執行事務,並判斷事務是否回滾;