防抖技術
防抖(Debouncing)是一種編程技術,用於控制事件處理常式的執行頻率。在使用者與界面互動頻繁的場景中,比如連續捲動、連續輸入等,如果每次互動都觸發事件處理常式,可能會導致效能問題或不必要的資料庫操作。
防抖技術透過設定一個延遲時間,在這段時間內,即使觸發了多次事件,事件處理常式也只會在延遲時間結束後執行一次。如果在這個延遲時間內再次觸發事件,那麽之前的延遲會被重設,重新開始計算延遲時間。這樣,只有最後一次事件觸發後,延遲時間結束後,事件處理常式才會執行。
防抖技術常用於以下場景
搜尋框輸入 :使用者連續輸入時,只有輸入停止一段時間後才觸發搜尋請求。
視窗調整大小 :使用者調整視窗大小時,只有調整結束後才執行相關操作。
捲動事件 :使用者捲動頁面時,只有捲動停止一段時間後才進行數據處理。
解決方案
在Web系統的互動設計中,表單送出是一個核心功能,但若不加以適當控制,使用者誤操作或網路的不穩定性都可能導致同一請求被重復發送,從而產生冗余數據。為了應對這一挑戰,我們可以從兩個層面進行最佳化:
前端防抖 :透過在使用者介面上實作按鈕的載入狀態(loading state),可以有效防止使用者因手抖而重復點選,從而避免前端生成多個請求。
後端防抖 :對於由網路波動引起的請求重發問題,前端的控制措施顯得不夠充分。因此,後端需要引入防抖邏輯,透過辨識請求的唯一性(例如使用請求ID或時間戳),確保即便在網路不穩定的情況下,同一請求也不會被重復處理。
防抖策略是確保Web系統穩定性和數據一致性的關鍵。前端的防抖措施提升了使用者體驗,而後端的防抖措施則保障了數據的準確性和系統的健壯性。兩者結合,可以構建一個更加穩定和使用者友好的Web套用環境。透過這種雙重保障,我們可以有效地減少因誤操作或網路問題導致的重復請求,維護系統的高效執行。
防抖場景
在Web系統中,並非所有介面都需要防抖,但以下型別的介面通常可以從防抖機制中獲益:
表單輸入場景
搜尋框輸入 :使用者在搜尋框中輸入時,可能會觸發即時搜尋或自動完成功能。防抖可以減少因快速輸入導致的頻繁請求。
表單輸入 :尤其是那些包含多個欄位或需要進行復雜驗證的表單,防抖可以避免使用者因誤操作而重復送出。
按鈕點選場景
按鈕點選類介面,如送出表單或保存設定,使用者在操作過程中可能會因各種原因頻繁點選按鈕,這不僅可能影響使用者體驗,還可能導致不必要的伺服器請求,增加系統負擔。
為了防止使用者因急促操作而導致的頻繁請求。透過設定一個短暫的等待時間,只有在使用者停止點選達到預設的時間閾值後,才會觸發實際的請求發送。這種方法不僅減少了伺服器的負擔,也避免了因重復請求而可能產生的數據錯誤或沖突。
捲動載入場景
在捲動載入類介面中,如下拉重新整理、上拉載入等,使用者的操作往往伴隨著連續的捲動動作。為了提升系統效率並避免因頻繁觸發而導致的效能問題。透過設定一個合理的時間間隔,只有在使用者捲動動作停止一段時間後,系統才會執行請求發送,從而實作智慧的請求管理。
如何防抖
使用共享緩存
圖片來源:https://developer.aliyun.com/article/1541251
使用分布式鎖
圖片來源:https://developer.aliyun.com/article/1541251
常見的分布式元件有Redis、Zookeeper等,但結合實際業務來看,一般都會選擇Redis,因為Redis一般都是Web系統必備的元件,不需要額外搭建。
程式碼實作
模仿一個使用者添加介面
目前資料庫表中沒有對
mobile
欄位做UK唯一索引限制,這就會導致每呼叫一次
userAdd
就會建立一個使用者,即使
mobile
相同。
demo_user
表結構
CREATETABLE`demo_user` (
`id`int(11) NOTNULL AUTO_INCREMENT,
`username`varchar(32) NOTNULL,
`mobile`char(13) NOTNULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDBDEFAULTCHARSET=utf8mb4;
分布式鎖
RedisLock.php
<?php
/**
* @desc RedisLock.php 描述資訊
* @author Tinywan(ShaoBo Wan)
* @date 2024/6/23 8:31
*/
declare(strict_types=1);
namespaceapp\common\service;
usesupport\Redis;
classRedisLock
{
// 分布式並行鎖
const DISTRIBUTED_CONCURRENT_LOCK = 'DISTRIBUTED_CONCURRENT_LOCK:';
/**
* @desc: 獲取鎖
* @param string $lock_name
* @param int $acquire_time
* @param int $lock_timeout
* @return bool|string
* @author Tinywan(ShaoBo Wan)
*/
publicstaticfunctiongetLockWithTimeout(string $lock_name, int $acquire_time = 3, int $lock_timeout = 20)
{
$identifier = md5($_SERVER['REQUEST_TIME'] . mt_rand(1, 10000000));
$lock_name = self::DISTRIBUTED_CONCURRENT_LOCK . $lock_name;
$lock_timeout = intval(ceil($lock_timeout));
$end_time = time() + $acquire_time;
while (time() < $end_time) {
$script = <<<luascript
local result = redis.call('setnx',KEYS[1],ARGV[1]);
if result == 1 then
return redis.call('expire',KEYS[1],ARGV[2])
elseif redis.call('ttl',KEYS[1]) == -1 then
return redis.call('expire',KEYS[1],ARGV[2]) -- 續租(renew)
else
return 0
end
luascript;
$result = Redis::eval($script, 1, $lock_name, $identifier, $lock_timeout);
if ($result === 1) {
return $identifier;
}
usleep(100000);
}
returnfalse;
}
/**
* @desc: 釋放鎖
* @param string $lock_name
* @param string $identifier
* @return bool
* @author Tinywan(ShaoBo Wan)
*/
publicstaticfunctionreleaseLock(string $lock_name, string $identifier): bool
{
$lock_name = self::DISTRIBUTED_CONCURRENT_LOCK . $lock_name;
while (true) {
$script = <<<luascript
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1]);
else
return 0
end
luascript;
$result = Redis::eval($script, 1, $lock_name, $identifier);
if ($result == 1) {
returntrue;
}
break;
}
returnfalse;
}
}
業務程式碼實作
<?php
/**
* @desc Demo.php 描述資訊
* @author Tinywan(ShaoBo Wan)
* @date 2024/6/23 20:14
*/
declare(strict_types=1);
namespaceapp\controller;
useapp\common\service\RedisLock;
usesupport\Request;
usesupport\Response;
useTinywan\ExceptionHandler\Exception\BadRequestHttpException;
useTinywan\ExceptionHandler\Exception\ServerErrorHttpException;
classDemoController
{
/**
* @desc 使用者添加
* @param Request $request
* @return Response
* @throws BadRequestHttpException
* @throws ServerErrorHttpException
* @author Tinywan(ShaoBo Wan)
*/
publicfunctionuserAdd(Request $request): Response
{
$param = $request->post();
/** 鎖名稱 */
$lockName = (string) $param['mobile'];
/** 嘗試獲取搶占鎖標識 */
$lockIdentifier = RedisLock::getLockWithTimeout($lockName);
/** 沒有拿到鎖說明已經有了請求了 */
if (false === $lockIdentifier) {
thrownew BadRequestHttpException('您的操作太快啦!請不要連續點選送出');
}
try {
/** 進行業務處理 */
\think\facade\Db::table('demo_user')->insert($param);
/** 進行業務處理 */
} catch (\Throwable $throwable) {
/** 釋放鎖 */
RedisLock::releaseLock($lockName, $lockIdentifier);
thrownew ServerErrorHttpException('系統異常:' . $throwable->getMessage());
}
/** 釋放鎖 */
RedisLock::releaseLock($lockName, $lockIdentifier);
return json(['code' => 200, 'msg' => 'success']);
}
}
RedisLock的核心思路就是搶鎖,當一次請求搶到鎖之後,對鎖加一個過期時間,在這個時間段內重復的請求是無法獲得這個鎖。
驗證分布式鎖
正確送出
後端異常送出
後端未響應之前送出
相同時間段內重復,鎖釋放剩余時間