本篇章是建立在上個篇章的基礎之上來實現我們的實例二「秒殺」案例。

廢話我們就不說了。

秒殺就是實實在在的高並發場景,一般都會經過「搶訂單」這個環節,而這個環節是最能考驗我們業務上提供的抗壓能力。處理不好很容易照成超賣現象,這個肯定不符合我們的業務需求,因為任何商品都會有庫存數量的上限。解決這個超賣其性能上注重的是優化:緩存、擴容和流量限制。當然這不是本篇章要講的內容。

超賣的原因:

庫存固定數額,當剩餘的商品還有 1 個時,突然系統發來了N條並發請,導致這些請求都讀取到了該商品僅有的 1 個餘量,然後這時都通過了這個餘量的判斷,最終導致超賣。見下圖:

大家可能會說悲觀鎖、樂觀鎖的處理機制。這裡只講下概念,不去給大家做實踐了,我們要說的是 redis 高並發處理場景。悲觀鎖就不講了,無法達到高並發的要求,不適合做秒殺,一點兒意義都沒有。

那什麼是樂觀鎖呢?

為什麼要講樂觀鎖,因為它跟我們今天要講的篇章有一定的關係。樂觀鎖假設認為數據一般情況下不會照成衝突,所以在數據進行提交更新的時候,才會正式對數據的衝突與否進行檢測,如果發現衝突了,則返回用戶錯誤的信息,讓用戶自己決定如何去做。

而redis當中的 watch 指令在redis 事物中提供了檢測的行為。為了檢測被 watch 的keys在是否有多個clients 同時改變引起衝突,這些keys 將會被監控。如果至少有一個被監控的key在執行exec命令錢被修改,整個事物將被回滾,則不會執行任何動作,從而保證了原子性操作,並且執行了exec會得到null的回復。

樂觀鎖的實現方法:

簡單一句話:就是使用數據版本記錄機制來實現。

意思是為數據增加一個版本標識,一般都是通過為資料庫表增加一個數字類型的「version」欄位來實現。當在讀取數據時,將version 欄位的值一同讀出,數據沒進行一次更新,version 的值就加 1。當我們提交數據更新的時,判斷資料庫表對應記錄值的當期版本信息與第一次取出來的 version 值進行對比,如果資料庫表當前版本號與第一次取出來的 version 值相等時才給與更新,否則認為是過期數據。思路是這樣的:用戶在獲取訂單時,更新版本號 version+1,在下單時用當前的版本號與庫中的版本號做對比檢測,存在則下單,不存在說明已被其他用戶更新了,那麼本次的秒殺就失敗結束了。

講到了這裡不知道大家有沒有理解,我們先看看這樣的實例:

在目錄 /home/wwwroot/default 創建 msa 項目文件

實例一

準備兩個php:index.php 主程序執行文件、mysql.php 資料庫連接文件於msa 文件中:

下面是mysql.php資料庫連接代碼【應用單例模式構建】

class Db
{
//存放實例
private static $_instance = null;
private $db;

private function __construct()
{
$dsn = "mysql:host=localhost;port=3306;dbname=msa";
$options = array(PDO::ATTR_PERSISTENT => false);
try {
$db = new PDO($dsn, "root", $pass, $options);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
//設置如果sql語句執行錯誤則拋出異常,事務會自動回滾
$db->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
//禁用prepared statements的模擬效果(防SQL注入)
$db->setAttribute(PDO::ATTR_TIMEOUT, 5);
//設置連接超時時間
} catch (PDOException $e) {
die(Connection failed: . $e->getMessage());
}
$db->exec(SET NAMES utf8mb4);
$this->db = $db;
}

public static function getInstance()
{
if (!(self::$_instance instanceof DB)) {
self::$_instance = new self();
}
return self::$_instance;
}

private function __clone()
{
}

public function getConn()
{
return $this->db;
}

public function fetch($sql)
{
$query = $this->db->prepare($sql);
$query->execute();
$data = $query->fetch(PDO::FETCH_ASSOC);
return $data;
}

public function cz($sql)
{
return $this->db->exec($sql);
}
}
//簡單地資料庫連接就寫完了。

注意:本篇章的內容是建立在上一節課的基礎之上,如果上個篇章沒有構建完成的話,請先構建上個篇章的內容。

資料庫 database msa 表 table seckill 欄位創建:
CREATE TABLE `seckill` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`num` int(11) NOT NULL,
`verson` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

index.php我們創建兩個index1.php、index2.php

在mysql終端執行插入數據命令:
mysql> insert into seckill (num) values (10);

index1.php代碼:

//查找數據表信息
include "./mysql.php";
$sql = "select * from `seckill` where id = 1";
$data = Db::getInstance()->fetch($sql);

if($data && $data[num] > 0)
{
sleep(1);
//模擬真實環境、讓程序休眠
$sql = "update seckill set num=num-1 where id = 1";
Db::getInstance()->cz($sql);
} else {
echo "暫無數據,請新增";
}

上述的代碼,在沒有看到前面跟大家說的「超賣現象」一眼望去好像一點毛病都沒有,可真實的情況呢?本篇章不是單純告訴大家如何去解決秒殺,有時候很普遍,我們在做程序時,看到這種寫法好像是沒有問題,可是在高並發的情況下,往往你很難保證數據的無誤性。所以這段代碼一點兒都不夠健壯,當然如果您的網站一天的pv量 都沒幾個鳥毛,不存在高並發的情況,那也就所謂了。好了現在我們執行下查找數據表信息:

mysql> select * from seckill;

好了我先執行下index1.php後再執行下結果看看

[root@instance-rttngj1u www]# php index1.php

這個時候我們的num 就變成了9:

那麼現在我們開始測試在高並發的場景下看看會發生什麼情況:

我們選擇apache 的ab 壓力測試工具:

執行指令:
查看ab 是否安裝:ab -V
如果沒有安裝的話執行指令:yum -y install httpd-tools

ab壓力測試的參數:

-c 500 並發數
-n 1000 請求數
還有更多的參數指令 請執行ab --help 查看,我們這裡只用 c 和 n
現在我們執行下高並發請求:
[root@instance-rttngj1u default]# ab -c500 -n1000 http://106.12.212.131/msa/index1.php
This is ApacheBench, Version 2.3 <$Revision: 1430300 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking 106.12.212.131 (be patient)
Completed 100 requests
......
然後我們在查看數據信息會有什麼變化

看到沒num 變成了num -13了。下面是給大家準備的錄製視頻是整個實現的全過程:

視頻封面

04:27秒殺超賣 過程演示

實例二

我們繼續前面說的樂觀鎖看下面的結構流程圖:

樂觀鎖順序執行
開啟watch監聽提交衝突

下面我們接著上個篇章的內容改造下適用本篇章的內容:

include "./mysql.php";
include RoundSlave.php;
$sentinel = [
[ip => 106.12.212.131,port => 22536],
[ip => 106.12.212.131,port => 22537],
[ip => 106.12.212.131,port => 22538]
];
$getSentinel = $sentinel[array_rand($sentinel)];
//讀redis數據
$redisRead = new Redis();
$redisRead->connect($getSentinel[ip],$getSentinel[port]);
$slaveInfo = $redisRead->rawCommand(SENTINEL,slaves,mymaster);

//寫redis數據
$redisWrite= new Redis();
$redisWrite->connect(106.12.212.131,6382);

$slaves=[];
foreach ($slaveInfo as $val){
$slaves[]=[ip=>$val[3],port=>$val[5]];
}

$slave = (new RoundSlave())->select($slaves);
try{
//讀
$redisRead->connect($slave[ip],$slave[port]);
$redisWrite->watch(sales); //監聽銷售量
$sales = $redisRead->get(sales); //獲取銷售量
$num = $redisRead->get(num); //獲取庫存數量【需要我們手動預定設置下】
if($sales >= $num)
{
exit(您來晚啦!);
} else {
//寫
$redisWrite->multi();//將命令放入到隊列中,且不執行。
$redisWrite->incr(sales);
sleep(1);
$result = $redisWrite->exec(); //按照命令的先後順序執行,如果監視的數據被修改了,命令執行就會失敗,返回空值

if($result)
{
$sql = "update seckill set num = num-1 where id = 1";
Db::getInstance()->cz($sql);
}
}
} catch (RedisException $e) {
file_put_contents(log.log,$e->getMessage());
} 1,5

代碼部署完以後,我們再次執行壓力測試會發現,沒有超賣現象的出現,

下面是完整的操作視頻過程:

視頻封面

03:46百萬秒殺解決超賣

好了,到此我們的哨兵集群就全部結束了,下個篇章就是更牛逼的redis-cluster集群了。

大家覺得不錯記得給個贊,請關注我的主頁和我的專欄,謝謝。


推薦閱讀:
相关文章