最近项目中有一个功能,就是转账红包(即时提现到支付宝,秒到账),每天限制用户提现一次,刚开始,每天和支付宝对账的时候都能对上,但是后来随着用户的增多,每天几万人提现,并发量上来了,发现有时候账单对不上,后来发现死锁了,然后使用事务并优化代码,这个问题解决了,随着时间的推移,突然发现有些用户提现了一次,支付宝给他连续转账了好几次,有的甚至几十次,然后看代码逻辑,怎么都没有发现问题,后来给支付宝提工单,支付宝那边回复是我这边连续调了转账接口好多次,然后就在思考这是为什么?
- 由于每个用户每天只能提现一次,所以请求一进来就我这边就会来查询这个用户是否提现过,如果体现过直接return,而且前端按钮也做了限制,只有我这边返回结果了才让用户点击下一次提现按钮,那到底是出现了什么问题那?
- 这件事就困扰了好久,突然有天早上发现,会不会这个用户采用非常规的方式,一直请求转账,一直疯狂请求,导致我这边数据库的数据还没修改过来(余额,是否提现成功等),下一次的请求就又过来了,然后支付宝接口就疯狂转账,想到了这里,我就在手机上下载了抓包工具,然后一瞬间向这个接口一次发送了99次请求,结果支付宝一下给我转了80笔钱,到这里,我瞬间明白了,那怎么处理那?
- 我们可以使用redis锁,就是请求一进来,我们就上锁,锁的名称就是user_id,体现成功或失败时释放锁即可,(一旦查询到这个user_id有锁就直接redis,这样就给我们数据库反应的时间了)
- 代码:
import uuid
import redis
#获取一个锁
# lock_name:锁定名称
# acquire_time: 客户端等待获取锁的时间
# time_out: 锁的超时时间
def acquire_lock(conn, lockname, identifier, expire=20):
if conn.setnx(lockname, identifier):
conn.expire(lockname, expire)
return identifier
elif not conn.ttl(lockname):
conn.expire(lockname, expire)
return False
#释放一个锁
def release_lock(conn, lockname, identifier):
pipe = conn.pipeline(True)
while True:
try:
pipe.watch(lockname)
if str(pipe.get(lockname).decode()) == str(identifier):
pipe.multi()
pipe.delete(lockname)
pipe.execute()
return True
pipe.unwatch()
break
# except redis.exceptions.WatchError:
except Exception as e
pass
# we lost the lock
return False
在视图中的应用:
# 先查询是否多次请求
redis_conn = django_redis.get_redis_connection('withdraw')
redis_add_lock = acquire_lock(redis_conn, user.id, user.id, expire=20)
if not redis_add_lock:
return APIResponse.fail(message='每日限1次,提现时间:8:00-24:00')
# 提现成功或失败时释放锁:
release_lock(redis_conn, user.id, user.id)
- 这样我再用抓包工具一瞬间请求99次就会提现一次了,所以,以后一旦牵扯到钱的问题,就要有redis锁和事务,这样才不会被恶意攻击