使用redis list類型解決卡券類獎品發放問題

問題描述:

卡券類獎品是指預先導入對應的卡券數據,然後將卡券一條條分配出去。

在併發高的時候,很容易出現多個人拿取同一張卡券的問題。

比如說A用戶拿道了卡券A,此時還沒提交,B用戶去數據庫裏拿取未發放的卡券,也拿到了卡券A。

此時一張卡券發給了兩個人,這種情況從業務上來講,肯定是不能接受的。

解決方案:

一、用數據庫鎖(不推薦)

方法:使用mysql數據庫的寫鎖。

優點:保證前一個人還沒拿到的時候,下一個也拿不到。

缺點:卡券發放流程是拿取後才知道對應的主鍵是什麼,也就是無法根據主鍵做行鎖,而是表鎖,併發極低,訪問量大的時候可能會造成功能不可用

二、隨機獲取(極不推薦)

方法:隨機從數據庫裏拿取一條卡券數據。

優點:不會鎖表,性能比較好

缺點:無異於飲鳩止渴,一開始數量多的時候不會重複,但是到後面,重複的概率會很高,並不能解決問題。

三、使用redis list類型(推薦)

方法:redis 是單線程服務,命令是一條條順序執行的,也就是不會出現併發問題。redis 的 list類型 的pop方法,可以保證不會出現 卡券發給多個人的情況。

優點:redis 的數據都在內存裏,讀取超快,可以支持很高的併發。

缺點:數據需要從數據庫裏初始化到redis裏,初始化會需要時間,如果沒有提前初始化的話,可能會出現有一兩秒的時間內,無法領取獎品。(具體看有多少條數據

python3代碼示例:

# coding:utf-8
# 初始化redis
import redis
from blind_box.setting import REDIS_IP, REDIS_PORT, REDIS_PWD, REDIS_DB

__author__ = 'mingv'

redis_config = {
    'host': REDIS_IP,
    'port': REDIS_PORT,
    'password': REDIS_PWD,
    'db': REDIS_DB
}

r = redis.Redis(**redis_config)
pool = redis.ConnectionPool(**redis_config)
red = redis.Redis(connection_pool=pool)
cache = red


class LotteryCache(object):
    """
    獎品信息緩存類,用於抽獎
    """

    @staticmethod
    def __get_prize_list_by_level(level, cache_key):
        """
        根據獎品等級獲取獎品
        :param level:
        :return: 1表示已經初始化過 2初始化成功
        """
        # 用redis 的incr 自增方法保證只會初始化一次,不會多次初始化
        init_cache_key = "_init" + str(level)
        # 獲取初始化數據
        init = cache.get(init_cache_key)
        # 如果沒有初始化
        if not init:
            # 自增,拿到返回數字
            num = cache.incr(init_cache_key)
            # 如果返回大於 1,說明不是第一次初始化
            if num > 1:
                return 1
        # 如果已經初始化了,返回1
        else:
            return 1
        # 獲取所有獎品
        prize_list = db.session.query(Prize).filter(Prize.level == level, Prize.bl_get == False).all()

        data_list = []
        # 使用redis 事務處理(不用也行,但會慢一些),不設置過期時間
        with cache.pipeline() as pipeline:
            # 開啓事務
            pipeline.multi()
            for prize in prize_list:
                # 解析json格式爲字符串 ,redis 只能保存字符串
                str_data = json.dumps(prize.to_json())
                data_list.append(str_data)
                # 寫入list 類型裏
                pipeline.lpush(cache_key, str_data)
            # 執行命令
            pipeline.execute()
        return 2

    def get_prize_by_level(self, level):
        """
        獲取獎品
        :return:
        """
        cache_key = CACHE_NAME + '_prize_level_' + str(level)
        # 不存在的話就初始化
        if not cache.exists(cache_key):
            num = self.__get_prize_list_by_level(level, cache_key)
            # 如果已經初始化過了,就返回1
            # 此時有兩種情況,一種是初始化過並且獎品發放完畢 另外一種是正在初始化中
            # 目前沒有想到好的方法來分辨這兩種情況,所以只好提前初始化,這樣就不會出現第二種情況了
            if num == 1:
                return num
        # 拿取一個
        prize_data = cache.lpop(cache_key)
        if prize_data:
            prize_data = json.loads(prize_data.decode())
        return prize_data

    @staticmethod
    def add_prize(prize_data):
        """
        用於當抽獎流程報錯時,回退獎品
        :return:
        """
        cache_key = CACHE_NAME + '_prize_list'
        cache.lpush(cache_key, prize_data)

 


lottery_cache =LotteryCache()

def get_prize():
    """
    拿取獎品示例
    """
    # 拿取一個獎品
    prize_data = lottery_cache.get_prize_by_level(level)
    # 如果沒有拿到獎品,或者返回爲 1,都認爲抽獎失敗了
    if not prize_data or prize_data == 1:
        return False, prize_data
    try:
        prize_id = prize_data.get("id")
        prize_data['openid'] = openid
        prize_data['create_time'] = datetime.datetime.now()
        # 更新數據庫裏的獎品數據
        Prize.query.filter_by(id=prize_id).update(prize_data)
        db.session.commit()
    # 異常捕獲
    except Exception as e:
        # 事務回滾
        db.session.rollback()
        # 刪除多餘字段
        if "openid" in prize_data:
            del prize_data['openid']
        if "bl_get" in prize_data:
            del prize_data['bl_get']

        # 將獎品重新加入list列表中
        lottery_cache.add_prize(json.dumps(prize_data))
        # 打印日誌,發送消息
        logger.info("prize_id %s ,openid %s 領取失敗  " % (prize_data.get("id"), openid))
        return False, None
    # 領取成功,返回獎品信息
    return True, prize_data

 

經過多次壓測,沒有出現超發以及重複發的情況。

 

完。

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章