1. 程式人生 > >redis秒殺系統資料同步(保證不多賣)

redis秒殺系統資料同步(保證不多賣)

秒殺系統需要保證東西不多賣,關鍵是在多個客戶端對庫存進行減操作時,必須加鎖。Redis中的Watch剛好可以實現一點。首先我們需要獲取當前庫存,只有庫存中的食物小於購物車的數目才能對庫存進行減。在高併發的情況下會出現某時刻查詢庫存夠的,但下一時刻另外一個執行緒下單了,對庫存進行減操作,剛好小於上個執行緒的購物車數目。照理現在的狀態是不能下單成功的,因為庫存已經不夠了,但上一執行緒仍然認為數量還夠,對庫存進行減操作,從而導致庫存出現負數的情況。如何避免?

Redis 中的watch可以在事務前對資料進行監控,如果在事務執行前,該資料發生改變,則事務不執行。剛好能滿足我們的要求。看了很多程式碼,對watch功能還不是很理解,因為網上很多寫的帖子都沒有明確指出多客戶端(理解之後發現還是有寫的),所以不明白的可以參見下面的例子,是用Java寫的。以下程式碼可以保證庫存不多賣。

在redis中設定一個鍵為mykey,值為1000的變數,

複製程式碼
public class Main {

    public static void main(String[] args) {
            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; @Override public void run() { // TODO Auto-generated method stub while (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 exception e.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開始一個個的執行被快取的命令,如果其中某個命令執行出錯了,那之前的命令並不會被回滾。

Redis保證從資料只加載一次

我這裡碰到的需求是一開始要從MySQL資料庫中匯入資料到Redis,由於有多臺伺服器,不進行控制會對資料進行多次載入,所以我們可以設定一個鍵值進行控制。

複製程式碼
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Response;
import redis.clients.jedis.Transaction;
public class Main {

    public static void main(String[] args) {
            new lock().start();
            new lock().start();
            new lock().start();
            new lock().start();
            new lock().start();
            new lock().start();
            new lock().start();
            new lock().start();
            new lock().start();
            new lock().start();
    }
}

class lock extends Thread{

    @Override
    public void run() {
        // TODO Auto-generated method stub
        System.out.println(Thread.currentThread().getName());
        Jedis jedis = null;
        try {
            jedis = RedisUtil.getJedis();
            if(jedis.setnx("look", "1") == 1){
                jedis.set("food", Thread.currentThread().getName());
            }else{
                System.out.println(Thread.currentThread().getName() + "未訪問");
            }
        } catch (Exception e) {
            // TODO: handle exception
            e.printStackTrace();
            RedisUtil.returnResource(jedis);
        }finally{
            RedisUtil.returnResource(jedis);
        }
    }

}
複製程式碼