Redis基本使用二(事務與鎖)
開發環境:
- JDK11
- Redis3.2
Redis事務機制:
與傳統的關係型資料庫類似,NoSQL也存在許多併發訪問的情況,因此出現瞭如何保證資料一致性的問題,處理的方式有很多。
針對不同的業務層次有不同的解決方案:
- 檢視層:前端來保證資料一致性,筆者對前端技術熟悉程度還不足以搞定,暫不討論;
- 業務層:可以使用執行緒同步來保證資料一致性;
- 持久層:在持久層解決資料一致性問題是最優的選擇,此時有悲觀鎖、樂觀鎖等解決方案;
對於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鎖機制應用流程:
- 定義Jedis物件;
- watch命令繫結監控的key;
- 開啟事務,設定執行命令;
- 執行事務,並判斷事務是否回滾;