1. 秒殺業務下需要解決的問題有哪些?
秒殺業務當然對於系統服務的可用性要求、以及數據的一致性是要求非常嚴格的,當然服務的高可以可以通過服務器集羣化來解決,而數據的一致的的話這個解決起來要複雜很多,可能我們習慣會用sychronized來解決秒殺系統下的超賣問題,但是這樣是有侷限性的,sychronized會使得我們的服務器的請求響應速度變慢,並且sychronized並不適合分佈式系統,而我們如果使用redis實現分佈式鎖就可以很好的解決以上問題。
2. 秒殺業務原始實現代碼
2.1 秒殺業務邏輯實現類 SecKillServiceImpl
@Service
public class SecKillServiceImpl implements SecKillService {
/**
* 國慶活動,皮蛋粥特價,限量100000份
*/
static Map<String,Integer> products;
static Map<String,Integer> stock;
static Map<String,String> orders;
static
{
/**
* 模擬多個表,商品信息表,庫存表,秒殺成功訂單表
*/
products = new HashMap<>();
stock = new HashMap<>();
orders = new HashMap<>();
products.put("123456", 100000);
stock.put("123456", 100000);
}
/**
* 查詢秒殺活動特價商品的信息
*
* @param productId
* @return
*/
@Override
public String querySecKillProductInfo(String productId) {
return "國慶活動,皮蛋粥特價,限量份"
+ products.get(productId)
+" 還剩:" + stock.get(productId)+" 份"
+" 該商品成功下單用戶數目:"
+ orders.size() +" 人" ;
}
/**
* 模擬不同用戶秒殺同一商品的請求
*
* @param productId
* @return
*/
@Override
public void orderProductMockDiffUser(String productId) {
//1.查詢該商品庫存,爲0則活動結束。
int stockNum = stock.get(productId);
if(stockNum == 0) {
throw new SellException(100,"活動結束");
}else {
//2.下單(模擬不同用戶openid不同)
orders.put(KeyUtil.genUniqueKey(),productId);
//3.減庫存
stockNum =stockNum-1;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
stock.put(productId,stockNum);
}
}
}
2.2 秒殺業務請求處理器實現
/**
* Created with IntelliJ IDEA.
* User: 李敷斌.
* Date: 2020-02-08
* Time: 16:01
* Explain: 秒殺商品請求處理器
*/
@RestController
@RequestMapping("/skill")
@Slf4j
public class SecKillController {
@Autowired
private SecKillService secKillService;
/**
* 查詢秒殺活動特價商品的信息
* @param productId
* @return
*/
@GetMapping("/query/{productId}")
public String query(@PathVariable String productId)throws Exception
{
return secKillService.querySecKillProductInfo(productId);
}
/**
* 秒殺,沒有搶到獲得"哎呦喂,xxxxx",搶到了會返回剩餘的庫存量
* @param productId
* @return
* @throws Exception
*/
@GetMapping("/order/{productId}")
public String skill(@PathVariable String productId)throws Exception
{
log.info("@skill request, productId:" + productId);
secKillService.orderProductMockDiffUser(productId);
return secKillService.querySecKillProductInfo(productId);
}
}
2.3 使用ab壓測來測試我們的秒殺下單接口
-
通過查詢接口查看目前商品餘量
可以看到當前餘量爲100000 -
手動請求下單連續下單幾次
可以發現數據準確無誤,這時因爲我們的的程序是能夠承受一定的併發量的,而且我們的手速也是不夠快的。 -
通過ab壓測,通過100個線程請求500次
ab -n 500 -u 100 請求地址
-
壓測後查詢商品秒殺情況
從上圖可以發現,在程序未做任何處理時,當請求激增的時候我們的商品超賣了,而且超賣數量還特別的多。
3. 使用sychronized同步關鍵字解決秒殺系統超賣問題
3.1 在秒殺業務實現類的下單方法上加入sychronized關鍵字。
/**
* 模擬不同用戶秒殺同一商品的請求
*
* @param productId
* @return
*/
@Override
public synchronized void orderProductMockDiffUser(String productId) {
//1.查詢該商品庫存,爲0則活動結束。
int stockNum = stock.get(productId);
if(stockNum == 0) {
throw new SellException(100,"活動結束");
}else {
//2.下單(模擬不同用戶openid不同)
orders.put(KeyUtil.genUniqueKey(),productId);
//3.減庫存
stockNum =stockNum-1;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
stock.put(productId,stockNum);
}
}
3.2 重啓應用程序,並查詢當前商品餘量信息
當前商品總餘量爲100000份
3.3 使用ab壓測,用100個線程發起500次請求
在測試的過程中明顯感覺服務器響應速度變慢了許多
3.4 請求完畢後查看結果
可以看到我們的數據是沒有問題的,雖然響應速度慢了點,但是至少還是完成了任務。
4. 使用redis實現分佈式鎖解決秒殺系統超賣問題
4.1. redis分佈式鎖加鎖與解鎖方法實現
package com.qingyun.wechat_ordersys.service.impl;
import com.lly835.bestpay.utils.StringUtil;
import com.qingyun.wechat_ordersys.service.RedisLockService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
/**
* Created with IntelliJ IDEA.
* User: 李敷斌.
* Date: 2020-02-08
* Time: 19:46
* Explain: 基於redis的分佈式鎖實現
*/
@Component
@Slf4j
public class RedisLockServiceImpl implements RedisLockService {
@Autowired
private RedisTemplate redisTemplate;
/**
* 加鎖方法
*
* @param key
* @param value 鎖超時時間=當前時間+加鎖時長
* @return 是否加鎖成功
*/
@Override
public boolean onLock(String key, String value) {
//使用redis命令setnf設置 如果值不存在就設置 如果存在就不設置
if(redisTemplate.opsForValue().setIfAbsent(key,value)){
return true;
}
String currentValue=redisTemplate.opsForValue().get(key).toString();
//判斷值是否過期
if(!StringUtil.isEmpty(currentValue)&&Long.valueOf(currentValue)<System.currentTimeMillis()){
//獲取上一個鎖的時間
String oldValue=redisTemplate.opsForValue().getAndSet(key,value).toString();
//防止多個線程都重新設值
if(!StringUtil.isEmpty(oldValue)&&oldValue.equals(currentValue)){
return true;
}
}
return false;
}
/**
* 解鎖方法
*
* @param key
* @param value
*/
@Override
public void offLock(String key,String value) {
try {
String currentValue=redisTemplate.opsForValue().get(key).toString();
if(!StringUtils.isEmpty(currentValue)&&value.equals(currentValue)){
//刪除鎖
redisTemplate.opsForValue().getOperations().delete(key);
}
}catch (Exception e){
log.error("【redis分佈式鎖解鎖操作】解鎖時發生異常");
}
}
}
4.2 修改秒殺訂單創建業務實現類
@Service
public class SecKillServiceImpl implements SecKillService {
@Autowired
private RedisLockServiceImpl redisLockService;
//設置一個常量 超時時間爲 10s
private static final Long TIMEOUT=10*1000L;
/**
省略部分業務代碼
**/
/**
* 模擬不同用戶秒殺同一商品的請求
*
* @param productId
* @return
*/
@Override
public void orderProductMockDiffUser(String productId) {
//加鎖
Long expireTime=System.currentTimeMillis()+TIMEOUT;
if(! redisLockService.onLock(productId,String.valueOf(expireTime))){
throw new SellException(ResultEnums.ON_REDIS_LOCK_FAILED);
}
//1.查詢該商品庫存,爲0則活動結束。
int stockNum = stock.get(productId);
if(stockNum == 0) {
throw new SellException(100,"活動結束");
}else {
//2.下單(模擬不同用戶openid不同)
orders.put(KeyUtil.genUniqueKey(),productId);
//3.減庫存
stockNum =stockNum-1;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
stock.put(productId,stockNum);
}
//解鎖操作
redisLockService.offLock(productId,String.valueOf(expireTime));
}
}
4.3 啓動項目,並查詢商品餘量信息
商品初始餘量爲100000份
4.4 通過ab壓測來測試該業務,測試請求500次下單
使用redis分佈式鎖之後明顯加快了請求的響應速度,這樣做其實更節省系統資源,因爲請求能夠快速的響應,而不需要等待其他請求的結束才能夠執行。
4.5 查詢商品餘量信息
可以看到信息準確無誤,沒有出現超賣的情況。