1. 程式人生 > >python 多併發競爭微信token重新整理問題的解決方案

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/

結果:
[[email protected] 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】
[[email protected] 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: 印象筆記負責過來的內容排版怎麼這麼難看呀。 以上