文章目录- 1 背景
- 2 问题及解决
- 3 源码
- 4 总结
1 背景
在系统设计与分析的这门课程中,我们要完成一个项目,是一个大学生通过填写问卷、帮拿快递等方式“挣闲钱”的app。这个App要求实名注册,身份认证的方案如下:以每个学生独有的学校邮箱作为认证注册的凭证,注册之前要先填写邮箱并通过邮箱获取验证码,输入正确验证码才能注册成功。
2 问题及解决
第一个重要的问题就是如何保存验证码?是保存在数据库中,还是直接保存在内存中,显然直接放在内存中效率是更高的。
另一个问题就是验证码的定时清理。由于验证码具有一定的时效性,并且在一次使用之后也不应该继续保存(浪费内存),所以如何定期地进行清理是一个重要的问题。最蠢的方法就是为每个验证码设置一个计时器,到时间就把它删掉,但这样显然很影响性能。那怎样才能使用更少的计时器呢?由于验证码的生成是有时间的先后顺序的,所以只要最老的那个还没过期,那么其余的肯定也没过期,所以思路就是维护一个优先队列,最老的放在前面,并给每个验证码附加一个创建时间戳,只需要定时判断前面的部分是否过期即可(当前时间减去创建时间是否大于某个阈值)。
但根据时间排序也会消耗性能?其实并不用真的维护什么优先队列,就一个链表就足够了,先插入的肯定就是比较老的。然后给邮箱发验证码时需要与将该邮箱与验证码关联起来,以便在收到注册请求是校验相对应的验证码,使用map非常合适。
综合以上需求,使用什么数据结构比较好呢?有没有一种基于hashmap数据结构,并且是按照插入顺序进行排序的,即先插入的在前面?还真有,它就是collections.OrderedDict。它的使用与dict差不多,只是实现的方式不一样,从Ordered就可看出它是有序的。
现在局势似乎明朗了,所以开始写代码:
代码语言:javascript复制import collections
codes = collections.OrderedDict()
def get_verification_code_(email):
'''
通过邮箱获取验证码,
验证码在一定时间内有效,超过这个期限则会自动删除
'''
global is_scheduler_running
res = {}
# 验证码还未过期的情况
if(email in codes):
res = {'error': 1, 'error_message': '原验证码未过期'}
print(str(res))
# 正常情况
else:
# 生成验证码并发送至邮箱
code = utils.send_email(rcptto=email) # 这是自己实现好的一个方法
if code == -1:
return str({'error': 1, 'error_message': '验证码发送失败'})
codes[email] = (code, time.time()) # 在本地记录验证码值
print('生成的验证码', codes[email])
res = {'error': 0, 'error_message': '验证码已发送'}
return str(res)
至于验证码的定时删除,这里使用 sched.scheduler 调度器作为计时器,其基本思路就是往调度器加一系列方法,调度器会对其进行调用,可以设定调用的时间。一开始先把删除验证码的方法放入,在该方法执行完之后再把该方法放入调度器,这样就相当于定时地循环调用删除验证码的方法。sched.scheduler的使用可以自行百度。
enter_event_and_run_scheduler的原始实现如下:
代码语言:javascript复制s = sched.scheduler(time.time, time.sleep) # 用来定时删除过期验证码的调度器
def enter_event_and_run_scheduler():
s.enter(2, 0, delete_invalid_codes)
t = Thread(target = s.run)
t.start()
原始的delete_invalid_codes方法如下:(但会有问题,稍后介绍)
代码语言:javascript复制def delete_invalid_codes():
'''
删除本地保存的过期(无效)的验证码。
OrderedDict按照插入的顺序排序,所以先创建验证码的一定在前面,从前面遍历删除直至遇到未过期的验证码为止
'''
global is_scheduler_running
for k in list(codes):
if(time.time() - codes[k][1] < time_limit):
break
if(k in codes):
try:
print('删除的验证码:', codes.pop(k))
except Exception as e:
print('Error:', e)
if(s.empty())
s.enter(time_limit, 0, delete_invalid_codes)
注册的代码如下:
代码语言:javascript复制def register_(email, password, student_id, sex, collage, grade, name, validate_code):
res = {}
if(email not in codes):
res = {'error': 1, 'error_message': '未获取验证码或验证码过期'}
elif(codes[email][0] != validate_code):
res = {'error': 1, 'error_message': '验证码错误'}
else:
'''
判断邮箱是否被注册
'''
error_code, error_message, openid = db_helper.sign_up_true(email, password, student_id, sex, collage, grade, name)
if(error_code == 0):
try:
codes.pop(email)
except Exception as e:
print('Error:', e)
res = {'error': str(error_code), 'error_message': error_message, 'data': {'openid': str(openid)}}
else:
res = {'error': str(error_code), 'error_message': error_message, 'data': {'openid': str(openid)}}
return str(res)
注:这里的register_和get_verification_code_是被flask的路由接口调用的,如下:
代码语言:javascript复制@app.route('/user/register/', methods=['POST'])
def register():
print(request)
return register_(request.form['email'], request.form['password'], request.form['student_id'],
request.form['sex'], request.form['collage'],
request.form['grade'], request.form['name'],
request.form['validate_code'])
@app.route('/user/get_verification_code/', methods=['POST'])
def get_verification_code():
return get_verification_code_(request.form['email'])
flask的使用也不做过多介绍,可自行学习,现在只需要知道在接收到注册的网络请求时,register_这个函数会被调用,接收到获取验证码的网络请求时get_verification_code_会被调用。
还有一个地方需要注意,就是其中对OrderedDict的删除操作,在注册成功时要删除验证码,也可能在过期时删除,这两者是多线程并发进行的,可能导致KeyError,所以使用try-except 捕获错误以防止程序出错终止。
在这么看来,以上代码好像可以了,跑起来确实也应该是没错的。但在基本功能实现后,考虑一个问题:当没有人进行注册时,验证码为空,那此时调度器就没必要再运行。貌似只需要在delete_invalid_codes中加一个判断:
代码语言:javascript复制if(len(codes) > 0 and s.empty()): # 若还有验证码,且调度队列为空,则继续将delete_invalid_codes加入调度器
s.enter(time_limit, 0, delete_invalid_codes)
这样当验证码为空时,调度器就结束了。但这又带来了一个麻烦,就是什么时候再次开启呢?很容易想到就是在下次二维码请求到来时再开启,可以使用一个布尔值 is_scheduler_running 判定调度器是否在运行,并且为其加锁,当每次请求二维码时,若调度器不在运行,在开启调度器,如下:
代码语言:javascript复制is_scheduler_running = False # 判定调度器是否正在运行
def get_verification_code_(email):
...
if(not is_scheduler_running): # 若调度器不在运行
enter_event_and_run_scheduler()
res = {'error': 0, 'error_message': '验证码已发送'}
return str(res)
在delete_invalid_codes中:
代码语言:javascript复制if(len(codes) > 0 and s.empty()): # 若还有验证码,且调度队列为空,则继续将delete_invalid_codes加入调度器
s.enter(2, 0, delete_invalid_codes)
else:
scheduler_lock.acquire()
is_scheduler_running = False
scheduler_lock.release()
修改后的 enter_event_and_run_scheduler 如下,由于 is_scheduler_running 处在临界区,需要对其进行加锁,否则if(s.empty()):
之后的代码可能会被多次运行。关于锁的概念,这里也不做过多介绍。
from threading import Lock
scheduler_lock = Lock()
def enter_event_and_run_scheduler():
scheduler_lock.acquire()
global is_scheduler_running
if(not is_scheduler_running):
is_scheduler_running = True
if(s.empty()):
s.enter(2, 0, delete_invalid_codes)
t = Thread(target = s.run)
t.start()
scheduler_lock.release()
以上工作做得差不多了,再想想一个问题:调度器在关闭之后,是否能在下次有需要时被正常触发?一般情况下是可以的,但由于这是多线程,会出现一个小问题:考虑一下这种情况,在所有验证码清空后,delete_invalid_codes运行到else:
之前,下一句即将执行的代码is_scheduler_running = False
还没有执行,然后来了一个验证码请求,线程刚好跳到那边去处理请求,此时is_scheduler_running还是等于true的,所以调度器没有被开启。然而线程切换回delete_invalid_codes继续运行之后,调度器就结束了,之后如果没有验证码请求,那内存中已有的验证码永远不会被删除,当然只要之后再来一个验证码请求,就没问题了,但不管怎样,这属于一个bug,可以解决当然解决了好。既然是在else跳转到别的地方去,然后跳转回来,那么只要在跳转回来之后再做一次判断,就可以把这个bug解决,如下:
def delete_invalid_codes():
...
else:
scheduler_lock.acquire()
is_scheduler_running = False
if(len(codes) > 0 and not is_scheduler_running): # 应对线程安全,此时可能有验证码加入,但调度器并未开启
enter_event_and_run_scheduler()
scheduler_lock.release()
本来到这里,笔者认为已经解决了,但在写这篇博客的时候突然发现,还是有问题的。可以看到enter_event_and_run_scheduler里面是开了一个新线程来运行调度器,而这里在delete_invalid_codes运行还没有结束之前,原来那个线程是还没有结束的,那么在enter新的方法进去之后,原来的线程会继续执行,并且这里又新开了一个线程,相当于有两个线程在执行一样的操作,这自然是十分没有必要的,也不是我们本来的意愿。所以,这里其实没必要重开一个线程,把代码改为:
代码语言:javascript复制def delete_invalid_codes():
...
else:
scheduler_lock.acquire()
is_scheduler_running = False
if(len(codes) > 0 and not is_scheduler_running and s.empty()): # 应对线程安全,此时可能有验证码加入,但调度器即将结束
is_scheduler_running = True
s.enter(2, 0, delete_invalid_codes)
scheduler_lock.release()
这个其实是犯了逻辑错误,以为调度器结束了要重新开启,但其实这里调度器并没有结束,因为delete_invalid_codes还没执行完呢。
3 源码
代码语言:javascript复制...
from threading import Thread, Lock
import time
import collections
import sched
...
codes = collections.OrderedDict()
s = sched.scheduler(time.time, time.sleep) # 用来定时删除过期验证码的调度器
scheduler_lock = Lock()
is_scheduler_running = False # 判定调度器是否正在运行
time_limit = 60 * 5
def register_(email, password, student_id, sex, collage, grade, name, validate_code):
res = {}
if(email not in codes):
res = {'error': 1, 'error_message': '未获取验证码或验证码过期'}
elif(codes[email][0] != validate_code):
res = {'error': 1, 'error_message': '验证码错误'}
else:
'''
判断邮箱是否被注册
'''
error_code, error_message, openid = db_helper.sign_up_true(email, password, student_id, sex, collage, grade, name)
if(error_code == 0):
try:
codes.pop(email)
except Exception as e:
print('Error:', e)
res = {'error': str(error_code), 'error_message': error_message, 'data': {'openid': str(openid)}}
else:
res = {'error': str(error_code), 'error_message': error_message, 'data': {'openid': str(openid)}}
return str(res)
def get_verification_code_(email):
'''
通过邮箱获取验证码,
验证码在一定时间内有效,超过这个期限则会自动删除
'''
global is_scheduler_running
res = {}
# 验证码还未过期的情况
if(email in codes):
res = {'error': 1, 'error_message': '原验证码未过期'}
print(str(res))
# 正常情况
else:
# 生成验证码并发送至邮箱
code = utils.send_email(rcptto=email)
if code == -1:
return str({'error': 1, 'error_message': '验证码发送失败'})
codes[email] = (code, time.time()) # 在本地记录验证码值
print('生成的验证码', codes[email])
# print(is_scheduler_running)
if(not is_scheduler_running): # 若调度器不在运行
enter_event_and_run_scheduler()
res = {'error': 0, 'error_message': '验证码已发送'}
return str(res)
def delete_invalid_codes():
'''
删除本地保存的过期(无效)的验证码。
OrderedDict按照插入的顺序排序,所以先创建验证码的一定在前面,从前面遍历删除直至遇到未过期的验证码为止
'''
global is_scheduler_running
for k in list(codes):
if(time.time() - codes[k][1] < time_limit):
break
if(k in codes):
try:
print('删除的验证码:', codes.pop(k))
except Exception as e:
print('Error:', e)
if(len(codes) > 0 and s.empty()): # 若还有验证码,且调度队列为空,则继续将delete_invalid_codes加入调度器
s.enter(time_limit, 0, delete_invalid_codes)
else:
scheduler_lock.acquire()
is_scheduler_running = False
if(len(codes) > 0 and not is_scheduler_running and s.empty()): # 应对线程安全,此时可能有验证码加入,但调度器即将结束
is_scheduler_running = True
s.enter(2, 0, delete_invalid_codes)
scheduler_lock.release()
def enter_event_and_run_scheduler():
scheduler_lock.acquire()
global is_scheduler_running
if(not is_scheduler_running):
is_scheduler_running = True
if(s.empty()):
s.enter(2, 0, delete_invalid_codes)
t = Thread(target = s.run)
t.start()
scheduler_lock.release()
4 总结
本文提出的方案主要利用了OrderedDict的先插入在前面的特性,来定时对验证码进行清理,只需要一个计时器;为了能在没有验证码时关闭计时器(调度器结束),需要解决多线程可能引发的一些问题。
虽然这可能并不是一个高效的方案(有许多判断,并且加了锁),并且由于贴贴补补,也不见得可靠,但也算是对定期清理内存和并发问题的一个思考,记录下来以便日后回忆和反思,也希望对读者有一点点帮助,或者让正在读这篇文章的大佬(没错就是你)嘲笑一番,以达到增强读者幸福感的目的,也足够了。