本篇章是建立在上個篇章的基礎之上來實現我們的實例二「秒殺」案例。
廢話我們就不說了。
秒殺就是實實在在的高並發場景,一般都會經過「搶訂單」這個環節,而這個環節是最能考驗我們業務上提供的抗壓能力。處理不好很容易照成超賣現象,這個肯定不符合我們的業務需求,因為任何商品都會有庫存數量的上限。解決這個超賣其性能上注重的是優化:緩存、擴容和流量限制。當然這不是本篇章要講的內容。
超賣的原因:
庫存固定數額,當剩餘的商品還有 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了。下面是給大家準備的錄製視頻是整個實現的全過程:
實例二
我們繼續前面說的樂觀鎖看下面的結構流程圖:
下面我們接著上個篇章的內容改造下適用本篇章的內容:
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
代碼部署完以後,我們再次執行壓力測試會發現,沒有超賣現象的出現,
下面是完整的操作視頻過程:
好了,到此我們的哨兵集群就全部結束了,下個篇章就是更牛逼的redis-cluster集群了。
大家覺得不錯記得給個贊,請關注我的主頁和我的專欄,謝謝。