python 多并发竞争微信token刷新问题的解决方案

看日志:
正常时候的日志:
2017-09-24 07:35:30,723 views.py[line:34] [INFO]  GetToken from Redis.
2017-09-24 07:35:31,342 views.py[line:24] [INFO]  【获取token】
2017-09-24 07:35:31,343 views.py[line:34] [INFO]  GetToken from Redis.
2017-09-24 07:35:35,156 views.py[line:24] [INFO]  【获取token】
2017-09-24 07:35:35,157 views.py[line:34] [INFO]  GetToken from Redis.
2017-09-24 07:35:40,285 views.py[line:24] [INFO]  【获取token】
2017-09-24 07:35:40,286 views.py[line:34] [INFO]  GetToken from Redis.
2017-09-24 07:35:52,522 views.py[line:24] [INFO]  【获取token】
2017-09-24 07:35:52,523 views.py[line:34] [INFO]  GetToken from Redis.
2017-09-24 07:35:52,523 views.py[line:51] [INFO]  【重置Token】 getToken中竟然拿到了过期的token,Ok...!
2017-09-24 07:35:52,524 tools.py[line:45] [INFO]  【生成新的token】
2017-09-24 07:35:52,934 tools.py[line:66] [INFO]  token=uX-Ss9sgpfcK5fAmxOomQevy4FZTQXB_FX6G0JjoNWGjws5ZJtK-QVXLcgXLooIcN4zutB8KehLQPV-0ZR3BhiD31jOy77M_d306XlIxqlbMrBuYYyrQg4xFHvNJW8MPSCAhABAWGE, expire_at=expire: 2017-09-24 09:35:52, ticket=kgt8ON7yVITDhtdwci0qeYKnTxnRCJqsQusUs77nYwUaOBEr--EY31LjMYstkPp15zQ0KTyT84KANjsx2UEu-A
2017-09-24 07:35:52,935 views.py[line:61] [INFO]  写到redis中...
2017-09-24 07:35:52,935 views.py[line:65] [INFO]  写到文件中...
2017-09-24 07:36:11,051 views.py[line:24] [INFO]  【获取token】
2017-09-24 07:36:11,052 views.py[line:34] [INFO]  GetToken from Redis.
2017-09-24 07:36:27,335 views.py[line:24] [INFO]  【获取token】
2017-09-24 07:36:27,335 views.py[line:34] [INFO]  GetToken from Redis.
2017-09-24 07:36:28,813 views.py[line:24] [INFO]  【获取token】
2017-09-24 07:36:28,814 views.py[line:34] [INFO]  GetToken from Redis.
2017-09-24 07:36:32,783 views.py[line:24] [INFO]  【获取token】

错误时候的日志:
2017-09-24 09:35:48,320 views.py[line:34] [INFO]  GetToken from Redis.
2017-09-24 09:35:48,992 views.py[line:24] [INFO]  【获取token】
2017-09-24 09:35:48,993 views.py[line:34] [INFO]  GetToken from Redis.
2017-09-24 09:35:51,360 views.py[line:24] [INFO]  【获取token】
2017-09-24 09:35:51,361 views.py[line:34] [INFO]  GetToken from Redis.
2017-09-24 09:35:51,814 views.py[line:24] [INFO]  【获取token】
2017-09-24 09:35:51,814 views.py[line:34] [INFO]  GetToken from Redis.
2017-09-24 09:35:53,318 views.py[line:24] [INFO]  【获取token】
2017-09-24 09:35:53,319 views.py[line:34] [INFO]  GetToken from Redis.
2017-09-24 09:35:53,319 views.py[line:51] [INFO]  【重置Token】 getToken中竟然拿到了过期的token,Ok...!
2017-09-24 09:35:53,319 tools.py[line:45] [INFO]  【生成新的token】
2017-09-24 09:52:03,673 tools.py[line:32] [INFO]  Current log level is : DEBUG
2017-09-24 09:52:03,796 MyCache.py[line:17] [INFO]  ===>redis畅通,切换到缓存模式!cache_flag = TRUE.
2017-09-24 09:52:03,797 wsgi.py[line:22] [INFO]  【初始化一个token】
2017-09-24 09:52:03,797 tools.py[line:45] [INFO]  【生成新的token】
2017-09-24 09:52:05,620 tools.py[line:32] [INFO]  Current log level is : DEBUG
2017-09-24 09:52:05,645 MyCache.py[line:17] [INFO]  ===>redis畅通,切换到缓存模式!cache_flag = TRUE.
2017-09-24 09:52:05,646 wsgi.py[line:22] [INFO]  【初始化一个token】
2017-09-24 09:52:05,646 tools.py[line:45] [INFO]  【生成新的token】
2017-09-24 09:52:08,796 tools.py[line:66] [INFO]  token=CSgJq-aPPLUYr_RoFbljb_Dia42HtEgQj77g55TWW1sVAIuOEvn5jjMOPwohmaTBQ73SDjBx2L1L0AifX0QNH3Rxvsb7YRlomapkypc9J7tVBnqo4w_izu-JWXN0Fs5XWZChAFAADG, expire_at=expire: 2017-09-24 11:52:04, ticket=kgt8ON7yVITDhtdwci0qeYKnTxnRCJqsQusUs77nYwVrkjNNRqAVnEJhMznAJIRjvn93qY1duo-sEO-gQlYr8A
2017-09-24 09:52:08,796 wsgi.py[line:25] [INFO]  写到文件中...
2017-09-24 09:52:08,797 wsgi.py[line:29] [INFO]  写到redis中...
2017-09-24 09:52:09,458 views.py[line:24] [INFO]  【获取token】
2017-09-24 09:52:09,460 views.py[line:34] [INFO]  GetToken from Redis.
2017-09-24 09:52:09,462 views.py[line:24] [INFO]  【获取token】
2017-09-24 09:52:09,463 views.py[line:34] [INFO]  GetToken from Redis.
2017-09-24 09:52:11,236 views.py[line:24] [INFO]  【获取token】
2017-09-24 09:52:11,237 views.py[line:34] [INFO]  GetToken from Redis.
2017-09-24 09:52:11,280 views.py[line:24] [INFO]  【获取token】

以下1,2,3,4... 是我的思考过程。。

1、
明显感觉到,在切换token的那一瞬间,正常情况下,是一个用户来请求,然后就完美度过这个切换token情况。
但是如果那一瞬间是3个用户来请求,则有问题啦。。

2、
为什么会冒出这句话:Current log level is : DEBUG   ===>redis畅通,切换到缓存模式!cache_flag = TRUE.
这是项目启动的时候才会说的话。而且还是重启两次?

后来发现是因为我用supervisor手动重启了wxtoken这个项目才打印这个日志(尴尬),然而为啥子是两次呢,是因为uwsgi就开启了两个进程。
root      8882 13690  0 10:23 ?        00:00:00 uwsgi /data/xxxx/wxtoken/uwsgi.ini --plugin Python
root      8888  8882  0 10:23 ?        00:00:00 uwsgi
可是我的uwsgi配置如下:
[uwsgi]
processes = 1
vhost = false
plugins = python
socket = 127.0.0.1:xxxx
master = true
enable-threads = true
workers = 1
wsgi-file = /data/xxxx/wxtoken/wxtoken/wsgi.py
chdir = /data/xxxx/wxtoken
home=/data/python_venv/wxtoken_venv/
listen=1024
workers=1 并且 processes =1, 就是单进程呀,为啥子有2个呢?
哦哦哦,原来是因为master=true,会有一个master进程+单个子进程=2个进程。爸爸管理n个孩子,如果kill爸爸就是杀了所有孩子。
先让master=false。因为我就是要单个进程即可。
附上uwsgi.ini参数说明(当然有些和我的配置出入,比如home就是程序运行的python环境目录):
socket:uwsgi监听的socket,可以为socket文件或ip地址+端口号(如0.0.0.0:9000),取决于nginx中upstream的设置
processes:同时启动uwsgi进程的个数,这个进程与nginx中的workers是不一样的,uwsgi中的每个进程每次只能处理一个请求(进程越多可以同时处理的请求越多),nginx采用的异步非阻塞的方式来处理请求的,每个进程可以接受处理多个请求。
chdir:在app加载前切换到当前目录
pythonpath:给PYTHONPATH 增加一个目录(或者一个egg),最多可以使用该选项64次。
module:加载指定的python WSGI模块(模块路径必须在PYTHONPATH里)
master:相当于master=true,启动一个master进程来管理其他进程,以上述配置为例,其中的4个uwsgi进程都是这个master进程的子进程,如果kill这个master进程,相当于重启所有的uwsgi进程
pidfile:在失去权限前,将master的pid写到当前文件中
daemonize:使进程在后台运行,并将日志打到指定的日志文件或者udp

3、
回到我的错误日志:
2017-09-24 09:35:53,319 tools.py[line:45] [INFO]  【生成新的token】
2017-09-24 09:52:03,673 tools.py[line:32] [INFO]  Current log level is : DEBUG
两句话差了快20分钟,在生成新的token这里就一直挂着了呢。我大概知道是网络请求有问题,要不把urllib改成request吧。
改为python更加推荐的requests库,加入超时参数,加入https不验证参数(有些时候验证https会报SSL错误,麻烦得紧)
# wp = urllib.urlopen(url)
# ret = json.loads(wp.read())
r = requests.get(url, timeout=3, verify=False)
ret = r.json()

以及

# jsapiTicketRequestData = {'type': 'jsapi', 'access_token': access_token}
# jsapiTicketRequestDataUrlencode = urllib.urlencode(jsapiTicketRequestData)
# jsapiTicketRequest = "https://api.weixin.qq.com/cgi-bin/ticket/getticket"
# jsapiTicketRequestGet = urllib2.Request(url=jsapiTicketRequest, data=jsapiTicketRequestDataUrlencode)
# jsapiTicketRequestGetData = urllib2.urlopen(jsapiTicketRequestGet)
# jsapiTicketRequestGetResult = jsapiTicketRequestGetData.read()
# ticket = json.loads(jsapiTicketRequestGetResult)['ticket']

payload = {'type': 'jsapi', 'access_token': access_token}
url = "https://api.weixin.qq.com/cgi-bin/ticket/getticket"
r = requests.get(url, params=payload, timeout=3, verify=False)
content = r.json()
ticket = content['ticket']

4、
修改完毕之后,手动让redis里面的token失效,因为是pickle dumps过的(如果是json dumps的话,能直接修改呀),我还得用程序去修改,修改。测试通过。
# coding=utf-8
import logging
import cPickle as pickle

import redis
redis_client = redis.Redis(host='localhost', port=6379, db=1, password='xxx')

value = {
    'access_token': 'e4ZUdlQQGRknsN7UfaBruFBKhj8Kj5_6kq7MhlkHscz5DiQSlT0RzQdMs-woooa-FW7JXlAzjUVPen4xTJrgWz6AohKY6KhO3aaPFVVnVz2sW7ATrUUgQtyj-GPrO6iWNDOaAJATJU',
    'access_token_expires_at':'1501228165',
    'ticket': 'kgt8ON7yVITDhtdwci0qeYKnTxnRCJqsQusUs77nYwUL-QgOjkakVZKqMbEctIhcpt',
}
name = 'wx_access_token'
value = pickle.dumps(value)
redis_client.set(name, value)

5、
ok,最后一步,还没解决如下问题,如果失效的那一瞬间,同时有10个请求过来。程序会发生什么事呢?
排个号: 1,2,3,4,5,6,7,8,9,10
因为这10个兄弟失效了嘛,依次处理,处理3的那一瞬间,token恢复正常,也写进了redis。
可是4-10这7个兄弟还不知道呀,它们依旧会走完流程,也就是不断地刷新token,不断得使之前的token失效,并且重新写入redis,写7次。
那么如果有11-30很多其他人这个时候来访问,其实他们只会拿到刚刚被这7个兄弟弄失效的token,但是不会重新去刷新token,因为我判断的依据是超时时间是否超过2小时,哈哈哈。
所以如果不加锁的话,影响也许就是十几二十的用户吧。

6、
ab测试下,上面的想法,结果大体一致,发现影响用户可能只有2,3人。
ab.exe -n 1200 -c 20 http://xxx.com/getToken/

结果:
[root@iZ9458z0ss9Z wxtoken]# cat wxtoken.log | grep 生成新     
2017-09-24 11:17:29,917 10818-140683547019072-MainThread tools.py[line:46] [INFO]  【生成新的token】
2017-09-24 11:17:41,358 10818-140683547019072-uWSGIWorker1Core0 tools.py[line:46] [INFO]  【生成新的token】
[root@iZ9458z0ss9Z wxtoken]#

7、
也有人问我为啥子不加定时器,其实之前是加了的,apscheduler,但是偶尔会报一些奇奇怪怪的错误,要么就是项目没有报错了但是定时器也不工作,很让人烦躁,索性去掉了,毕竟又不是非要用定时器,用定时器也不是非要用这货。
其实我别的项目用到了更加靠谱的定时器:celery,但是我不想在这个简简单单的地方引入这么重型的哥们。
(当然如果哪一天项目重要程度升级,并发很高balabala,我就换celery咯。)
至于现在,我想用python自己去解决这个刷新token的事。

8、
加个锁吧。python的threading,condition。
大致代码如下:
def get_token_from_srouce():
    """
    从数据源获取token
    要么是缓存,要么是文件。。
    :return:
    """
    response_data = {}
    try:
        item = mycache.get('wx_access_token')
    except Exception, ex:
        logging.error(ex)
        item = None

    # 从redis拿
    if item:
        logging.info("GetToken from Redis.")
        dic = item
        response_data['access_token'] = dic['access_token']
        response_data['access_token_expires_at'] = dic['access_token_expires_at']
        response_data['ticket'] = dic['ticket']
    # 从文件中拿
    else:
        logging.info("GetToken from %s." % settings.accessTokenFile)
        with open(settings.accessTokenFile, 'r') as f:
            response_data['access_token'] = f.readline().strip('\n')
            response_data['access_token_expires_at'] = f.readline().strip('\n')
            response_data['ticket'] = f.readline().strip('\n')
    return response_data

def set_token_to_soruce(dic):
    """
    把数据写入数据源
    :param dic:
    :return:
    """
    # 写到redis中
    logging.info("写到redis中...")
    mycache.set('wx_access_token', dic)

    # 写到文件中
    logging.info("写到文件中...")
    with open(settings.accessTokenFile, 'w') as tokenFile:
        tokenFile.write(dic['access_token'] + '\n')
        tokenFile.write(str(dic['access_token_expires_at']) + '\n')
        tokenFile.write(dic['ticket'] + '\n')

# 返回access_token
def get_token(request):
    logging.info(u"【获取token】")
    my_token_dic = get_token_from_srouce()

    # 如果获取的时间戳显示token过期,则reset一下
    expires_at = int(my_token_dic['access_token_expires_at'])
    now = int(time.time())
    if now > expires_at:
        logging.info(u"【重置Token】 getToken中竟然拿到了过期的token,Ok...!")

        settings.condition.acquire()

        # 双重判断
        my_token_dic = get_token_from_srouce()
        expires_at = int(my_token_dic['access_token_expires_at'])
        now = int(time.time())
        if now > expires_at:
            my_token_dic = get_new_token()
            set_token_to_soruce(my_token_dic)

        settings.condition.notify_all()
        settings.condition.release()

    # 顺便更新返回的信息
    response_data = {}
    response_data['access_token'] = my_token_dic['access_token']
    response_data['access_token_expires_at'] = my_token_dic['access_token_expires_at']
    response_data['ticket'] = my_token_dic['ticket']
    response_data['code'] = 1
    response_data['msg'] = 'Ok!'

    return HttpResponse(json.dumps(response_data), content_type="application/json")




总结: 
1、加个锁机制,保证在失效的那一瞬间,那并发的几十个请求都等着,而且必须是双重检查锁机呦。。
2、完美解决方案是定时器和getToken服务分离,定时器每个小时去刷一次token,getToken服务不管别的,来了就返回redis或者文件里面的value即可。
但我就不!

ps: 印象笔记负责过来的内容排版怎么这么难看呀。

以上

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值