在前段工作中,曾幾次收到超級話題積分漏記的用戶反饋。通過源碼的閱讀分析後,發現問題出在高並發分布式場景下的計數器上。計數器的值會影響用戶當前行為所獲得積分的大小。比如,當用戶在某超級話題下連續第n(n即計數器的值)次進行轉發帖子時,將會獲得與n相關的分數。然而,在第一次改進後問題依然存在。所以,這次在之前的基礎上,通過使用MySQL變量的途徑來解決該問題。
MySQL變量一共分為兩大類:用戶自定義變量和系統變量。如下:
本文涉及的內容為用戶自定義會話變量,若對其他分類無感,請點擊這裡。
PS:用戶定義的會話變量和系統定義的會話變量有什麼區別?
局部變量一般用於SQL的語句塊中,比如存儲過程中的begin和end語句塊。其作用域僅限於該語句塊內。生命周期也僅限於該存儲過程的調用期間。
1 drop procedure if exists add; 2 create procedure add 3 ( 4 in a int, 5 in b int 6 ) 7 begin 8 declare c int default 0; 9 set c = a + b; 10 select c as c; 11 end;
上述存儲過程中定義的變量c就是局部變量。
會話變量即為服務器為每個客戶端連接維護的變量。在客戶端連接時,使用相應全局變量的當前值對客戶端的回話變量進行初始化。設置會話變量不需要特殊權限,但客戶端只能更改自己的會話變量。其作用域與生命周期均限於當前客戶端連接。
會話變量的賦值:
1 set session var_name = value; 2 set @@session.var_name = value; 3 set var_name = value;
會話變量的查詢:
1 select @@var_name; 2 select @@session.var_name; 3 show session variables like "%var%";
全局變量影響服務器整體操作。當服務器啟動時,它將所有全局變量初始化為默認值。這些默認值可以在選項文件中或在命令行中指定的選項進行更改。要想更改全局變量,必須具有SUPER權限。全局變量作用於server的整個生命周期,但是不能跨重啟。即重啟後所有設置的全局變量均失效。要想讓全局變量重啟後繼續生效,需要更改相應的配置文件。
全局變量的設置:
1 set global var_name = value; //注意:此處的global不能省略。根據手冊,set命令設置變量時若不指定GLOBAL、SESSION或者LOCAL,默認使用SESSION 2 set @@global.var_name = value; //同上
全局變量的查詢:
1 select @@global.var_name; 2 show global variables like "%var%";
你可以利用SQL語句將值存儲在用戶自定義變量中,然後再利用另一條SQL語句來查詢用戶自定義變量。這樣以來,可以再不同的SQL間傳遞值。
用戶自定義變量的聲明方法形如:@var_name,其中變量名稱由字母、數字、“.”、“_”和“$”組成。當然,在以字符串或者標識符引用時也可以包含其他字符(例如:@’my-var’,@”my-var”,或者@`my-var`)。
用戶自定義變量是會話級別的變量。其變量的作用域僅限於聲明其的客戶端鏈接。當這個客戶端斷開時,其所有的會話變量將會被釋放。
用戶自定義變量是不區分大小寫的。
使用SET語句來聲明用戶自定義變量:
1 SET @var_name = expr[, @var_name = expr] ...
在使用SET設置變量時,可以使用“=”或者“:=”操作符進行賦值。
當然,除了SET語句還有其他賦值的方式。比如下面這個例子,但是賦值操作符只能使用“:=”。因為“=”操作符將會被認為是比較操作符。
mysql> SET @t1=1, @t2=2, @t3:=4; mysql> SELECT @t1, @t2, @t3, @t4 := @t1+@t2+@t3; +------+------+------+--------------------+ | @t1 | @t2 | @t3 | @t4 := @t1+@t2+@t3 | +------+------+------+--------------------+ | 1 | 2 | 4 | 7 | +------+------+------+--------------------+
用戶變量的類型僅限於:整形、浮點型、二進制與非二進制串和NULL。在賦值浮點數時,系統不會保留精度。其他類型的值將會被轉成相應的上述類型。比如:一個包含時間或者空間數據類型(temporal or spatial data type)的值將會轉換成一個二進制串。
如果用戶自定義變量的值以結果集形式返回,系統會將其轉換成字符串形式。
如果查詢一個沒有初始化的變量,將會以字符串類型返回NULL。
用戶自定義變量可以用於很多上下文中。但是目前並不包括那些顯式使用常量的表達式中,比如SELECT中的LIMIT子句,或者LOAD DATA中的IGNORE N LINES的字句中。
通常來說,除了在SET語句中,不要再同一個SQL語句中同時賦值並使用同一個用戶自定義變量。舉個變量自增的例子,下面的是沒問題的:
1 SET @a = @a + 1;
對於其他語句,比如SELECT,也許會得到期望的效果,但這真心不靠譜。比如下面的語句,也許你自然地會認為MySQL會先執行@a的值,然後再進行賦值操作:
1 SELECT @a, @a:=@a+1, ...;
然而,用戶自定義變量表達式的計算順序還沒有定義呢。
除此之外,還有另一個問題。變量的默認返回類型由語句開始時的類型決定的,正如下面的例子:
1 mysql> SET @a='test'; 2 mysql> SELECT @a,(@a:=20) FROM tbl_name;
上述的SELECT語句中,MySQL會報告給客戶端第一列的字段類型為字符串,同時將所有對@a變量的使用均轉換為字符串處理,盡管在SELECT語句中將@a變量設置為數字類型。在SELECT語句執行後,@a變量才會在下一個語句中識別為數字類型。
為了避免上述問題的發生,要麼不在同一個語句中同時賦值並使用變量,要麼在使用之前,將變量設置為0,0.0,或者”,以確定它的數據類型。
在SELECT語句中,在每一個select表達式被發送給客戶端後,才會進行計算。這就意味著,在形如HAVING,GROUP BY和ORDER BY只句中有使用在當前select表達式定義的變量的情況下,該語句將不會得到如期的效果。
1 mysql> SELECT (@aa:=id) AS a, (@aa+3) AS b FROM tbl_name HAVING b=5;
上述在HAVING只句中使用了在當前的select列表中定義的別名b,其使用了變量@aa。這條語句並不會得到如期的效果:@aa變量為上一次SQL語句執行的結果集中的ID值,並非當前的。
超級話題積分系統
積分行為:如轉發、評論超級話題下的帖子、簽到某超級話題或者帖子被其他人回復等行為。
積分行為次數:產生積分行為的累計次數。
用戶在某超級話題下,第N次產生累計積分的行為,如轉發微博,會增加該用戶在該超級話題下的積分總數。具體的積分規則見長文章。
曾有用戶反饋說超級話題積分有漏記的情況:為什麼我評論了卻沒有加分;為什麼轉發了超級話題帖子沒有加分等等。隨後,我們立即通過查詢後台的積分記錄發現,會看到轉發行為在第5次時,積分的增加卻為0。這顯然是不正常的。
首先,排除了根據積分行為的次數來計算積分值的問題。比如第5次轉發微博應增加6分。這塊的規則,利用二分法寫死在程序裡面,也做過單元測試,不會有問題。那麼,問題就鎖定在這個積分行為的次數。
首先來看看積分次數的獲取:
1 public static function find($uid, $aid, $status) { 2 $sql = 'SELECT * FROM '.self::table($aid).' WHERE uid = ? AND aid = ? AND status = ?'; 3 return Comm_Db::d(Comm_Db::DB_BASIC)->fetchRow($sql, array($uid, $aid, $status)); 4 }
然後,利用上述find()方法來取得該用戶在某超級話題下的某積分行為的累計次數。這是有問題的,在於讀於從庫,但並不保證從庫的值是最新的,所以導致當前獲取的積分行為次數並不一定是正確的(小於等於實際的值)。
隨後,程序會根據當前的次數計算積分值,並分別更新積分值和該行為的積分行為次數值。
所以,這次利用MySQL的用戶自定義會話變量的方式,來解決上述問題。
1 public static function incCounter($uid, $aid, $status) { 2 $db = Comm_Db::d(Comm_Db::DB_BASIC); 3 $sql = "UPDATE ". self::table($aid) ." SET `ctn_counter`=@ctn_counter:=`ctn_counter`+1 WHERE `uid` = ? AND `aid` = ? AND `status` = ?"; 4 $db->execute($sql, array($uid, $aid, $status)); 5 $sql = "SELECT @ctn_counter"; 6 $rs = $db->fetchOne($sql, null, true); 7 return $rs; 8 }
改進後,如上述函數,程序將先進行調用incCounter()函數,將當前的積分行為次數自增,並將值存入當前變量中。隨後,立即將其讀取並返回給PHP進行積分處理。這樣一來,就保證了積分行為次數的正確性。
在這次的“填坑”過程中,使用了MySQL變量解決了MySQL主從服務同步延遲的問題。這篇文章也算是對於MySQL用戶自定義變量深入學習的記載。
除此之外,仍有個問題,用戶自定義的會話變量是存在進程內存中的。但是,是存在客戶端進程中還是服務端進程中的呢?
參考文章:
文章來源:胡旭博客 => 深入MySQL用戶自定義變量:使用詳解及其使用場景案例
轉載請注明出處,違者必究!