簡介:
當高併發訪問某個接口的時候,如果這個接口訪問的數據庫中的資源,並且你的數據庫事務級別是可重複讀(Repeatable read)的話,確實是沒有線程問題的,因爲數據庫鎖的級別就夠了;但是如果這個接口需要訪問一個靜態變量、靜態代碼塊、全局緩存的中的資源或者redis中的資源的時候,就會出現線程安全的問題。
案例:
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<String, Integer> product = new HashMap();
//訂單表
private static HashMap<String, String> orders = new HashMap();
//庫存表
private static HashMap<String, Integer> 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) + ",現在成功下單" + orders.size()
+ ",剩餘庫存" + stock.get(product_id) + "件";
}
/**
* 下單
*
* @param product_id
* @return
*/
public String order1(String product_id) {
if (stock.get(product_id) == 0) {
return "活動已經結束了";
//已近買完了
} else {
//還沒有賣完
try {
//模擬操作數據庫
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);
}
}
如上圖所述,我現在需要限購10000個商品id爲123的商品,如果什麼操作也不做,直接訪問靜態資源你們覺得會有問題嗎?我們使用apache_ab來模擬一下高併發情況,下面是發起100個請,併發量是50的情況:
問題: 可以看到,下單數和庫存加起來明顯超過了商品總數,這是一種超賣現象,在java角度來說就是線程不安全現象。
解決1: 學過javase的小夥伴應該都能想到使用synchronized關鍵字,強行同步。
/**
* 下單
*
* @param product_id
* @return
*/
public synchronized String order2(String product_id) {
if (stock.get(product_id) == 0) {
return "活動已經結束了";
//已近買完了
} else {
//還沒有賣完
try {
//模擬操作數據庫
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方法)實現線程安全,因爲redis是單線程,能保證線程的安全性,而且redis強大的讀寫能力能提高效率。
/**
* 高併發沒問題,效率還行
*
* @param product_id
* @return
*/
public String order3(String product_id) throws CongestionException {
/**
* redis加鎖
*/
String value = System.currentTimeMillis() + 10000 + "";
if (!redisLock.lock1(product_id, value)) {
//系統繁忙,請稍後再試
throw new CongestionException();
}
//##############################業務邏輯#################################//
if (stock.get(product_id) == 0) {
return "活動已經結束了";
//已近買完了
} else {
//還沒有賣完
try {
//模擬操作數據庫
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
orders.put(MyStringUtils.getuuid(), product_id);
stock.put(product_id, stock.get(product_id) - 1);
}
//##############################業務邏輯#################################//
/**
* redis解鎖
*/
redisLock.unlock(product_id, value);
return select_info(product_id);
}
/**
* 用redis實現分佈式鎖
*/
@Component
public class RedisLock {
@Autowired
private StringRedisTemplate redisTemplate;
//加鎖
public boolean lock1(String key, String value) {
//setIfAbsent相當於jedis中的setnx,如果能賦值就返回true,如果已經有值了,就返回false
//即:在判斷這個key是不是第一次進入這個方法
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相同的兩個線程同時訪問這個方法的時候纔會出現線程問題,這似乎是很完美了。但是有沒有想過,萬一處理業務邏輯的代碼塊中出現了異常,直接拋了出去,那解鎖的代碼就再也不會被執行了,也就是出現了死鎖現象。
改進1:
public boolean lock2(String key, String value) {
//setIfAbsent相當於jedis中的setnx,如果能賦值就返回true,如果已經有值了,就返回false
//即:在判斷這個key是不是第一次進入這個方法
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;
}
缺點: 使用超時時間來解決死鎖問題,但是又出現新的問題,就是當有兩個商品id相同的線程同時執行到了②這一行代碼,這時候兩個線程同時獲取鎖,這樣一來任然存在線程安全問題了。。。
改進2:
/**
* 加鎖
*/
public boolean lock3(String key, String value) {
//setIfAbsent相當於jedis中的setnx,如果能賦值就返回true,如果已經有值了,就返回false
//即:在判斷這個key是不是第一次進入這個方法
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;
}
解釋: 如果兩個線程同時調用這個方法,當同時走到①的時候,無論怎麼樣都有一個線程會先執行②這一行,假設線程1先執行②這行代碼,那redis中key對應的value就變成了value,然後線程2再執行②這行代碼的時候,獲取到的old_value就是value,那麼value顯然和他上面獲取的current_value是不一樣的,則線程2是沒法獲取鎖的。
說明: 雖然100個請求只有2個成功下單的,但是耗時卻明顯變小了,而且線程也是安全的,只是絕大部分因爲沒有拿到鎖而沒有搶到限購的商品,但也做了人性化的提醒,個人覺得還是可以接受的!
————————————————
版權聲明:本文爲CSDN博主「一隻仰望天空的菜鳥」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/tuesdayma/article/details/82751790