问题来源
gotit一直正常运行,现在不是成绩查询的时间了,访问量也少了不少。趁着这段时间打算重构下网站代码,当初上线之后总是添加功能或者应对正方教务系统,现在的代码非常乱。还有就是,正方查询的页面加载很慢,主要是登录之前的pre_login
操作, 需要先访问一次正方教务系统,获取初始化用户状态(此处保存的是一个对象, 下面称其为zf)和中文验证码图片。
相对其他页面的响应来说, 这段时间是最长的, 粗略测试了下,其时间在0.6~1.5秒之间不等,多次刷新的时候需要等待的时间更长,并且有的时候正方系统会响应不及时,导致无法获取用户状态。
开始时,我打算将zf缓存到redis中,每一时刻都缓存若干个zf供用户使用,设置好过期时间,删除过期的zf,如果缓存的zf小于一定数目则进行获取,如此供用户使用,此时用户每次进行GET的时候仅从服务器本机获取数据而不用访问正方系统,一定会快很多。后来发现,并不是所有的对象都是能序列化(pickle)的。在《python标准库》中提到:
套接字、文件句柄、数据库链接以及其他运行时状态依然依赖操作系统或者其他进程的对象可能无法用一种有意义的方式保存。
需要缓存的zf对象就不能进行序列化,因为他依赖urllib2提供的opener,这样就不能使用redis进行缓存,只能将其缓存在内存中,比如保存在一个全局字典中,现在模拟登录的方法就是使用了一个字典保存用户get时获得的zf,用户post信息(学号、密码和验证码)后从字典中将zf读取出来,继续处理。
多线程加速
开始
全部操作需要四个线程:
-
DaemonThread(D) : 守护线程
- 检测缓存字典中zf的数目
- 如果小于某一特定数值时则创建缓存(即创建zf)
- 如果大于该数目则等待
-
CreateThread(CR) : 创建缓存进程
- 守护线程的子线程
- 缓存不足的时候守护线程调用该线程创建缓存
-
CheckThread(CH) : 过期处理
- 检查缓存字典中的键值对是否过期
- 过期则POP
-
MainThread(M) : 调用上述线程
- 多线程调用
-
all_clients: 缓存没有使用过的zf
- 保存缓存的字典
- 结构: time_md5 -> (zf, viewstate, timeStart )
-
used_clients: 经过get操作的zf
- 用户进行get操作时,使用的键将从前者中pop到这个字典中,供后来的POST操作时模拟登录使用
- 结构: time_md5 -> (zf, viewstate, timeStart )
-
login_succeed: 登录成功的zf
- 供用户二次查询时使用
- 结构: time_md5 -> (zf, xh, timeStart )
-
temp_clients: 创建缓存时的中转字典
- 现将生成的zf放到这里,生成一定数量的时候,与
all_clients
合并,避免all_clients
长期加锁。
- 现将生成的zf放到这里,生成一定数量的时候,与
相关变量
viewstate : 正方教务系统post时需要的一个参数,类似csrf
timeStart : 键值对的每一次操作都会将该值更新为当前时间
xh : 查询者的学号, 供二次查询时其他查询使用
多线程相关
所有的线程都将setDaemon(True)
, 保证主线程结束后其他线程也相应结束。
这里使用了threading
提供的Condition
对象,condition提供了对复杂线程的同步问题的一个解决方案, 也叫做条件变量,除了提供加锁、需求(acquire)和释放(release)方法外,还提供了停止等待(wait)和事件通知(notify)方法。
在这里可以先判断字典的键值对数目,小于定值的时候D则创建若干个CR线程创建缓存, 每个CR线程创建一个zf即停止。如果大于定值则等待(不过这里不能时候wait
,应为wait会一直阻塞,直到被notify), 这里让其继续判断。
还有一个要说的是,python里字典和元组都是线程安全的,因为python对这两种数据结构的操作都是在字节码层次。
具体方法
网站程序启动时调用M, M调用D & CH, D一直监控all_clients键值对的数目, CH每隔定值(与正方系统的验证码过期时间有关)检查一遍前三个字典中所有键的创建时间,大于定值则POP。
对字典中的键值对进行过期检查的时候,现将整个字典copy一下,对copy的字典进行检测,将过期的键输出到列表中,统一pop,避免长期加锁,这也算浪费空间节约时间的一种方法吧。
每个键的创建时间在每一次进行操作的时候都会变更为当前时间,就像正常上网时,每个一段时间刷新一次网页那登录就永远不会过期。