java实现分布式redis锁_使用redis实现分布式锁
# 簡介:
當(dāng)高并發(fā)訪問某個接口的時候,如果這個接口訪問的數(shù)據(jù)庫中的資源,并且你的數(shù)據(jù)庫事務(wù)級別是可重復(fù)讀(Repeatable read)的話,確實是沒有線程問題的,因為數(shù)據(jù)庫鎖的級別就夠了;但是如果這個接口需要訪問一個靜態(tài)變量、靜態(tài)代碼塊、全局緩存的中的資源或者redis中的資源的時候,就會出現(xiàn)線程安全的問題。
## 案例:
**github地址:** https://github.com/mzd123/mywy/tree/master/src/main/java/com/mzd/mywy/service
```
@RestController
public class MsController {
@Autowired
private MsService msService;
@RequestMapping("/select_info.do")
public String select_info(String product_id) {
return msService.select_info(product_id);
}
@RequestMapping("/order.do")
public String order(String product_id) throws CongestionException {
return msService.order1(product_id);
}
}
```
```
@Service
public class MsService {
@Autowired
private RedisLock redisLock;
//商品詳情
private static HashMap product = new HashMap();
//訂單表
private static HashMap orders = new HashMap();
//庫存表
private static HashMap stock = new HashMap();
static {
product.put("123", 10000);
stock.put("123", 10000);
}
public String select_info(String product_id) {
return "限量搶購商品XXX共" + product.get(product_id) + ",現(xiàn)在成功下單" + orders.size()
+ ",剩余庫存" + stock.get(product_id) + "件";
}
/**
* 下單
*
* @param product_id
* @return
*/
public String order1(String product_id) {
if (stock.get(product_id) == 0) {
return "活動已經(jīng)結(jié)束了";
//已近買完了
} else {
//還沒有賣完
try {
//模擬操作數(shù)據(jù)庫
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
orders.put(MyStringUtils.getuuid(), product_id);
stock.put(product_id, stock.get(product_id) - 1);
}
return select_info(product_id);
}
}
```
如上圖所述,我現(xiàn)在需要限購10000個商品id為123的商品,如果什么操作也不做,直接訪問靜態(tài)資源你們覺得會有問題嗎?我們使用apache_ab來模擬一下高并發(fā)情況,下面是發(fā)起100個請,并發(fā)量是50的情況:

**問題:** 可以看到,下單數(shù)和庫存加起來明顯超過了商品總數(shù),這是一種超賣現(xiàn)象,在java角度來說就是線程不安全現(xiàn)象。
**解決1:** 學(xué)過javase的小伙伴應(yīng)該都能想到使用synchronized關(guān)鍵字,強行同步。
```
/**
* 下單
*
* @param product_id
* @return
*/
public synchronized String order2(String product_id) {
if (stock.get(product_id) == 0) {
return "活動已經(jīng)結(jié)束了";
//已近買完了
} else {
//還沒有賣完
try {
//模擬操作數(shù)據(jù)庫
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
orders.put(MyStringUtils.getuuid(), product_id);
stock.put(product_id, stock.get(product_id) - 1);
}
return select_info(product_id);
}
```

**缺點:**
1、我們可以明顯的看到速度變慢了,從原來的0.535秒變到了10.956秒,那是因為synchronized放這個方法只允許單線程訪問了。
2、synchronized是粗粒度的控制了線程安全,即:如果我這個商品id不一樣的線程,理論上是可以同時訪問這個方法的,但是加上了synchronized之后,無論商品id是否一樣,兩個線程都是沒法同時訪問這個方法的。
**解決2:** 使用redis分布式鎖(主要使用了redis中的setnx和getset方法,這兩個方法在redisTemplate分別是setIfAbsent和getAndSet方法)實現(xiàn)線程安全,因為redis是單線程,能保證線程的安全性,而且redis強大的讀寫能力能提高效率。
```
/**
* 高并發(fā)沒問題,效率還行
*
* @param product_id
* @return
*/
public String order3(String product_id) throws CongestionException {
/**
* redis加鎖
*/
String value = System.currentTimeMillis() + 10000 + "";
if (!redisLock.lock1(product_id, value)) {
//系統(tǒng)繁忙,請稍后再試
throw new CongestionException();
}
//##############################業(yè)務(wù)邏輯#################################//
if (stock.get(product_id) == 0) {
return "活動已經(jīng)結(jié)束了";
//已近買完了
} else {
//還沒有賣完
try {
//模擬操作數(shù)據(jù)庫
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
orders.put(MyStringUtils.getuuid(), product_id);
stock.put(product_id, stock.get(product_id) - 1);
}
//##############################業(yè)務(wù)邏輯#################################//
/**
* redis解鎖
*/
redisLock.unlock(product_id, value);
return select_info(product_id);
}
```
```
/**
* 用redis實現(xiàn)分布式鎖
*/
@Component
public class RedisLock {
@Autowired
private StringRedisTemplate redisTemplate;
//加鎖
public boolean lock1(String key, String value) {
//setIfAbsent相當(dāng)于jedis中的setnx,如果能賦值就返回true,如果已經(jīng)有值了,就返回false
//即:在判斷這個key是不是第一次進(jìn)入這個方法
if (redisTemplate.opsForValue().setIfAbsent(key, value)) {
//第一次,即:這個key還沒有被賦值的時候
return true;
}
return false;
}
//解鎖
public void unlock(String key, String value) {
try {
if (MyStringUtils.Object2String(redisTemplate.opsForValue().get(key)).equals(value)) {
redisTemplate.opsForValue().getOperations().delete(key);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
```
**缺點:** 這個方法看上去沒什么問題,而且只有商品id相同的兩個線程同時訪問這個方法的時候才會出現(xiàn)線程問題,這似乎是很完美了。但是有沒有想過,萬一處理業(yè)務(wù)邏輯的代碼塊中出現(xiàn)了異常,直接拋了出去,那解鎖的代碼就再也不會被執(zhí)行了,也就是出現(xiàn)了死鎖現(xiàn)象。
**改進(jìn)1:**
```
public boolean lock2(String key, String value) {
//setIfAbsent相當(dāng)于jedis中的setnx,如果能賦值就返回true,如果已經(jīng)有值了,就返回false
//即:在判斷這個key是不是第一次進(jìn)入這個方法
if (redisTemplate.opsForValue().setIfAbsent(key, value)) {
//第一次,即:這個key還沒有被賦值的時候
return true;
}
String current_value = redisTemplate.opsForValue().get(key);//①
if (!MyStringUtils.Object2String(current_value).equals("")
//超時了
&& Long.parseLong(current_value) < System.currentTimeMillis()) {;//②
//返回true就能解決死鎖
return true;
}
return false;
}
```
**缺點:** 使用超時時間來解決死鎖問題,但是又出現(xiàn)新的問題,就是當(dāng)有兩個商品id相同的線程同時執(zhí)行到了②這一行代碼,這時候兩個線程同時獲取鎖,這樣一來任然存在線程安全問題了。。。
**改進(jìn)2:**
```
/**
* 加鎖
*/
public boolean lock3(String key, String value) {
//setIfAbsent相當(dāng)于jedis中的setnx,如果能賦值就返回true,如果已經(jīng)有值了,就返回false
//即:在判斷這個key是不是第一次進(jìn)入這個方法
if (redisTemplate.opsForValue().setIfAbsent(key, value)) {
//第一次,即:這個key還沒有被賦值的時候
return true;
}
String current_value = redisTemplate.opsForValue().get(key);
if (!MyStringUtils.Object2String(current_value).equals("")
//超時了
&& Long.parseLong(current_value) < System.currentTimeMillis()) {//①
String old_value = redisTemplate.opsForValue().getAndSet(key, value);//②
if (!MyStringUtils.Object2String(old_value).equals("")
&& old_value.equals(current_value)) {
return true;
}
}
return false;
}
```
**解釋:** 如果兩個線程同時調(diào)用這個方法,當(dāng)同時走到①的時候,無論怎么樣都有一個線程會先執(zhí)行②這一行,假設(shè)線程1先執(zhí)行②這行代碼,那redis中key對應(yīng)的value就變成了value,然后線程2再執(zhí)行②這行代碼的時候,獲取到的old_value就是value,那么value顯然和他上面獲取的current_value是不一樣的,則線程2是沒法獲取鎖的。


**說明:** 雖然100個請求只有2個成功下單的,但是耗時卻明顯變小了,而且線程也是安全的,只是絕大部分因為沒有拿到鎖而沒有搶到限購的商品,但也做了人性化的提醒,個人覺得還是可以接受的!
創(chuàng)作挑戰(zhàn)賽新人創(chuàng)作獎勵來咯,堅持創(chuàng)作打卡瓜分現(xiàn)金大獎總結(jié)
以上是生活随笔為你收集整理的java实现分布式redis锁_使用redis实现分布式锁的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 王者荣耀女英雄泳装汇总 美女英雄夏日福利
- 下一篇: java取出连续子串_JAVA :在给定