问题描述:
卡券类奖品是指预先导入对应的卡券数据,然后将卡券一条条分配出去。
在并发高的时候,很容易出现多个人拿取同一张卡券的问题。
比如说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
经过多次压测,没有出现超发以及重复发的情况。
完。