在上一篇文章中,我們詳細的介紹了對於下單流量不算高的系統,可以通過請求唯一ID
+資料表增加唯一索引約束
這種方案來實現防止介面重複提交!
隨著業務的快速增長,每一秒的下單請求次數,可能從幾十上升到幾百甚至幾千。
面對這種下單流量越來越高的場景,此時資料庫的存取壓力會急劇上升,上面這套方案全靠資料庫來解決,會特別吃力!
對於這樣的場景,我們可以選擇引入快取中介軟體來解決,可選的元件有 redis、memcache 等。
下面,我們以引入redis
快取資料庫伺服器,向大家介紹具體的解決方案!
我們先來看一張圖,這張圖就是本次方案的核心流程圖。
實現的邏輯,流程如下:
redis
中再返回給前端,前端將唯一 ID 值埋點在頁面裡面redis
的分散式鎖服務,對請求 ID 在限定的時間內進行加鎖,如果加鎖成功,繼續後續流程;如果加鎖失敗,說明服務正在處理,請勿重複提交redis
中的請求唯一 ID 清理掉引入快取服務,防止重複提交的大體思路如上,實踐程式碼如下!
小編的專案是基於SpringBoot
版本進行構建,新增相關的redis
依賴環境如下:
<!-- 引入springboot -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.0.RELEASE</version>
</parent>
......
<!-- Redis相關依賴包,採用jedis作為使用者端 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</exclusion>
<exclusion>
<artifactId>lettuce-core</artifactId>
<groupId>io.lettuce</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
特別注意:由於每個專案環境不一樣,具體的依賴包需要和工程版本號匹配!
在全域性設定application.properties
檔案中,新增redis
相關服務設定如下
# Redis資料庫索引(預設為0)
spring.redis.database=1
# Redis伺服器地址
spring.redis.host=127.0.0.1
# Redis伺服器連線埠
spring.redis.port=6379
# Redis伺服器連線密碼(預設為空)
spring.redis.password=
# Redis伺服器連線超時設定
spring.redis.timeout=1000
# 連線池設定
spring.redis.jedis.pool.max-active=8
spring.redis.jedis.pool.max-wait=1000
spring.redis.jedis.pool.max-idle=8
spring.redis.jedis.pool.min-idle=0
spring.redis.jedis.pool.time-between-eviction-runs=100
在使用redis
之前,請確保redis
伺服器是啟動狀態,並且能正常存取!
@RestController
@RequestMapping("api")
public class SubmitTokenController {
/**
* SubmitToken過期時間
*/
private static final Integer EXPIRE_TIME = 60;
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 獲取getSubmitToken
* @return
*/
@RequestMapping("getSubmitToken")
public ResResult getSubmitToken(){
String uuid = UUID.randomUUID().toString();
//存入redis
stringRedisTemplate.opsForValue().set(uuid, uuid, EXPIRE_TIME, TimeUnit.SECONDS);
return ResResult.getSuccess(uuid);
}
}
首先建立一個@SubmitToken
註解,通過這個註解來進行方法代理攔截!
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface SubmitToken {
}
編寫方法代理服務,增加防止重複提交的驗證,實現了邏輯如下!
@Order(1)
@Aspect
@Component
public class SubmitTokenAspect {
private static final Logger LOGGER = LoggerFactory.getLogger(SubmitTokenAspect.class);
/**
* 獲取分散式鎖等待時間,單位秒
*/
private static final Long LOCK_REDIS_WAIT_TIME = 3L;
/**
* 分散式鎖字首
*/
private static final String LOCK_KEY_PREFIX = "SUBMIT:TOKEN:LOCK";
/**
* 預設鎖對應的值
*/
private static final String DEFAULT_LOCK_VALUE = "DEFAULT_LOCK_VALUE";
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private RedisLockService redisLockService;
/**
* 方法呼叫環繞攔截
*/
@Around(value = "@annotation(com.example.submittoken.config.annotation.SubmitToken)")
public Object doAround(ProceedingJoinPoint joinPoint){
HttpServletRequest request = getHttpServletRequest();
if(Objects.isNull(request)){
return ResResult.getSysError("請求引數不能為空!");
}
String submitToken = request.getHeader("submitToken");
if(StringUtils.isEmpty(submitToken)){
return ResResult.getSysError("submitToken不能為空!");
}
//檢查submitToken是否存在
String submitTokenValue = stringRedisTemplate.opsForValue().get(submitToken);
if(StringUtils.isEmpty(submitTokenValue)){
return ResResult.getSysError(ResResultEnum.SUBMIT_ERROR_MESSAGE);
}
//嘗試加鎖
String lockKey = LOCK_KEY_PREFIX + submitToken;
boolean lock = redisLockService.tryLock(lockKey, DEFAULT_LOCK_VALUE, Duration.ofSeconds(LOCK_REDIS_WAIT_TIME));
if(!lock){
return ResResult.getSysError("服務正在處理,請勿重複提交!");
}
try {
//繼續執行後續流程
Object result = joinPoint.proceed();
//任務執行成功,清除submitToken快取
stringRedisTemplate.delete(submitToken);
return result;
} catch (CommonException e) {
return ResResult.getSysError(e.getMessage());
} catch (Throwable e) {
LOGGER.error("業務處理髮生異常,錯誤資訊:",e);
return ResResult.getSysError(ResResultEnum.DEFAULT_ERROR_MESSAGE);
} finally {
//執行完畢之後,手動將鎖釋放
redisLockService.releaseLock(lockKey, DEFAULT_LOCK_VALUE);
}
}
/**
* 獲取請求物件
* @return
*/
private HttpServletRequest getHttpServletRequest(){
RequestAttributes ra = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes sra = (ServletRequestAttributes)ra;
HttpServletRequest request = sra.getRequest();
return request;
}
}
部分校驗邏輯用到了redis
分散式鎖,具體實現邏輯如下:
/**
* redis分散式鎖服務類
* 採用LUA指令碼實現,保證加鎖、解鎖操作原子性
*
*/
@Component
public class RedisLockService {
/**
* 分散式鎖過期時間,單位秒
*/
private static final Long DEFAULT_LOCK_EXPIRE_TIME = 60L;
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 嘗試在指定時間內加鎖
* @param key
* @param value
* @param timeout 鎖等待時間
* @return
*/
public boolean tryLock(String key,String value, Duration timeout){
long waitMills = timeout.toMillis();
long currentTimeMillis = System.currentTimeMillis();
do {
boolean lock = lock(key, value, DEFAULT_LOCK_EXPIRE_TIME);
if (lock) {
return true;
}
try {
Thread.sleep(1L);
} catch (InterruptedException e) {
Thread.interrupted();
}
} while (System.currentTimeMillis() < currentTimeMillis + waitMills);
return false;
}
/**
* 直接加鎖
* @param key
* @param value
* @param expire
* @return
*/
public boolean lock(String key,String value, Long expire){
String luaScript = "if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then return redis.call('expire', KEYS[1], ARGV[2]) else return 0 end";
RedisScript<Long> redisScript = new DefaultRedisScript<>(luaScript, Long.class);
Long result = stringRedisTemplate.execute(redisScript, Collections.singletonList(key), value, String.valueOf(expire));
return result.equals(Long.valueOf(1));
}
/**
* 釋放鎖
* @param key
* @param value
* @return
*/
public boolean releaseLock(String key,String value){
String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
RedisScript<Long> redisScript = new DefaultRedisScript<>(luaScript, Long.class);
Long result = stringRedisTemplate.execute(redisScript, Collections.singletonList(key),value);
return result.equals(Long.valueOf(1));
}
}
@RestController
@RequestMapping("order")
public class OrderController {
@Autowired
private OrderService orderService;
/**
* 下單
* @param request
* @return
*/
@SubmitToken
@PostMapping(value = "confirm")
public ResResult confirm(@RequestBody OrderConfirmRequest request){
//呼叫訂單下單相關邏輯
orderService.confirm(request);
return ResResult.getSuccess();
}
}
整套方案完全基於redis
來實現,同時結合redis
的分散式鎖來實現請求限流,之所以選擇redis
,是因為它是一個記憶體資料庫,效能比關係型資料庫強太多,即使每秒的下單請求量在幾千,也能很好的應對,為關係型資料庫起到降壓作用!
特別注意的地方:使用redis
的分散式鎖,推薦單機環境,如果redis
是叢集環境,可能會導致鎖短暫無效!
隨著下單流量逐漸上升,通過查詢資料庫來檢查當前服務請求是否重複提交這種方式,可能會讓資料庫的請求查詢頻率變得非常高,資料庫的壓力會倍增。
此時我們可以引入redis
快取,將通過查詢資料庫來檢查當前請求是否重複提交這種方式,轉移到通過查詢快取來檢查當前請求是否重複提交,可以很好的給資料庫降壓!
作者:程式設計師志哥
出處:www.pzblog.cn
資源:微信搜【Java極客技術】關注我,回覆 【cccc】有我準備的一執行緒序必備計算機書籍、大廠面試資料和免費電子書。 希望可以幫助大家提升技術和能力。