東西不多賣
秒殺系統需要保證東西不多賣,關鍵是在多個用戶端對庫存進行減操作時,必須加鎖。Redis中的Watch剛好可以實現一點。首先我們需要擷取當前庫存,只有庫存中的食物小於購物車的數目才能對庫存進行減。在高並發的情況下會出現某時刻查詢庫存夠的,但下一時刻另外一個線程下單了,對庫存進行減操作,剛好小於上個線程的購物車數目。照理現在的狀態是不能下單成功的,因為庫存已經不夠了,但上一線程仍然認為數量還夠,對庫存進行減操作,從而導致庫存出現負數的情況。如何避免。
Redis 中的watch可以在事務前對資料進行監控,如果在事務執行前,該資料發生改變,則事務不執行。剛好能滿足我們的要求。看了很多代碼,對watch功能還不是很理解,因為網上很多寫的文章都沒有明確指出多用戶端(理解之後發現還是有寫的),所以不明白的可以參見下面的例子,是用Java寫的。以下代碼可以保證庫存不多賣。
在redis中設定一個鍵為mykey,值為100的變數
package com.test.redis;import java.util.List;import redis.clients.jedis.Jedis;import redis.clients.jedis.Transaction;public class RedisLocal {public static void main(String[] args) {RedisUtil.getJedis().set("mykey", "100");new MyThread().start();new MyThread().start();new MyThread().start();new MyThread().start();new MyThread().start();new MyThread().start();new MyThread().start();new MyThread().start();new MyThread().start();new MyThread().start();}}class MyThread extends Thread {Jedis jedis = null;@Overridepublic void run() {// TODO Auto-generated method stubwhile (true) {System.out.println(Thread.currentThread().getName());jedis = RedisUtil.getJedis();try {int stock = Integer.parseInt(jedis.get("mykey"));if (stock > 0) {jedis.watch("mykey");Transaction transaction = jedis.multi();transaction.set("mykey", String.valueOf(stock - 1));List<Object> result = transaction.exec();if (result == null || result.isEmpty()) {System.out.println("Transaction error...");// 可能是watch-key被外部修改,或者是資料操作被駁回}} else {System.out.println("庫存為0");break;}} catch (Exception e) {// TODO: handle exceptione.printStackTrace();RedisUtil.returnResource(jedis);} finally {RedisUtil.returnResource(jedis);}}}}
對於Redis事務來說行不通,因為在exec命令之前,所有的命令都被Redis緩衝起來了,根本就拿不到balance的值。那類似這種需要基於已經存在的某個值的事務在Redis中如何?呢。答案是Watch命令:
redis.watch('balance')balance = redis.get('balance')if (balance < amtToSubtract) { redis.unwatch()} else { redis.multi() redis.decrby('balance', amtToSubtract) redis.incrby('debt', amtToSubtract) redis.exec()}
通俗點講,watch命令就是標記一個鍵,如果標記了一個鍵,在提交事務前如果該鍵被別人修改過,那事務就會失敗,這種情況通常可以在程式中重新再嘗試一次。像上面的例子,首先標記了鍵balance,然後檢查餘額是否足夠,不足就取消標幟,並不做扣減;足夠的話,就啟動事務進行更新操作,如果在此期間鍵balance被其它人修改,那在提交事務(執行exec)時就會報錯,程式中通常可以捕獲這類錯誤再重新執行一次,直到成功。
Redis事務失敗後不支援復原 與資料庫事務很重要的一個區別是Redis事務在執行過程中出錯後不會復原。在exec命令後,Redis Server開始一個個的執行被緩衝的命令,如果其中某個命令執行出錯了,那之前的命令並不會被復原。
RedisUtil
import redis.clients.jedis.Jedis;import redis.clients.jedis.JedisPool;import redis.clients.jedis.JedisPoolConfig;public final class RedisUtil {// Redis伺服器IPprivate static String ADDR = "192.168.1.8";// Redis的連接埠號碼private static int PORT = 6379;// 訪問密碼private static String AUTH = "cl";// 可用串連執行個體的最大數目,預設值為8;// 如果賦值為-1,則表示不限制;如果pool已經分配了maxActive個jedis執行個體,則此時pool的狀態為exhausted(耗盡)。private static int MAX_ACTIVE = 1024;// 控制一個pool最多有多少個狀態為idle(閒置)的jedis執行個體,預設值也是8。private static int MAX_IDLE = 200;// 等待可用串連的最大時間,單位毫秒,預設值為-1,表示永不逾時。如果超過等待時間,則直接拋出JedisConnectionException;private static int MAX_WAIT = 10000;private static int TIMEOUT = 10000;// 在borrow一個jedis執行個體時,是否提前進行validate操作;如果為true,則得到的jedis執行個體均是可用的;private static boolean TEST_ON_BORROW = true;private static JedisPool jedisPool = null;/** * 初始化Redis串連池 */static {try {JedisPoolConfig config = new JedisPoolConfig();config.setMaxIdle(MAX_IDLE);config.setTestOnBorrow(TEST_ON_BORROW);jedisPool = new JedisPool(config, ADDR, PORT, TIMEOUT, AUTH);} catch (Exception e) {e.printStackTrace();}}/** * 擷取Jedis執行個體 * * @return */public synchronized static Jedis getJedis() {try {if (jedisPool != null) {Jedis resource = jedisPool.getResource();return resource;} else {return null;}} catch (Exception e) {e.printStackTrace();return null;}}/** * 釋放jedis資源 * * @param jedis */public static void returnResource(final Jedis jedis) {if (jedis != null) {jedisPool.returnResource(jedis);}}}