問題描述:
卡券類獎品是指預先導入對應的卡券數據,然後將卡券一條條分配出去。
在併發高的時候,很容易出現多個人拿取同一張卡券的問題。
比如說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
經過多次壓測,沒有出現超發以及重複發的情況。
完。