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

本文探讨了微信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: 印象笔记负责过来的内容排版怎么这么难看呀。

以上

<think>好的,我现在需要帮助用户解决如何用Python编写监控股票价格变动通过微信发送消息的问题。首先,我得理清用户的需求。用户想要一个程序,能够实时监控股票价格,在价格达到某些条件时通过微信发送通知。我需要考虑以下几个方面: 首先,如何获取股票数据。用户提到了引用[4]中的股票数据爬虫项目,比如使用stockholm库。不过可能需要更实时的数据源,比如新浪财经、雅虎财经的API,或者第三方库如akshare、tushare。需要检查这些库的可用性和是否需要API密钥。比如tushare需要注册获取token,而akshare可能不需要,这样更方便用户快速开始。 其次,如何监控价格变化。这部分可能需要定时任务,比如每隔一段时间获取最新价格,然后与设定的条件比较。Python中有schedule库可以定时执行任务,或者使用APScheduler。同时,需要考虑网络请求的稳定性,可能需要异常处理,比如重试机制。 然后是微信消息的发送。引用[2]提到使用itchat库,但可能存在维护问题。用户可能需要替代方案,比如使用企业微信的API,或者第三方库如wxpy,不过wxpy也可能面临同样的问题。引用[3]中的Requests库可以用来发送HTTP请求到企业微信的API,这可能更可靠。根据引用[2]的建议,最好采用企业微信的方式,因为个人微信接口不稳定,容易被限制。 接下来是整体代码结构。用户需要整合数据获取、条件检查和消息发送三个部分。可能需要一个配置文件来存储监控的股票代码和阈值,或者直接在代码中设置。此外,日志记录和错误处理也很重要,确保程序长时间运行的稳定性。 还要考虑依赖的库,比如requests用于HTTP请求,apscheduler用于定时任务,pandas用于数据处理(如果需要的话),以及企业微信需要的库。用户需要安装这些库,可能需要编写安装说明。 然后,如何通过企业微信发送消息。需要引导用户注册企业微信,创建应用,获取企业ID、应用ID和密钥,这些步骤可能需要详细说明,因为用户可能不熟悉企业微信的设置。 代码示例方面,应该分步骤展示:获取股票数据、设置监控逻辑、发送微信消息。例如,使用akshare获取实时数据,使用apscheduler定时执行检查,当价格超过阈值时调用企业微信的API发送消息。 测试代码时,需要注意股票代码的格式(比如新浪是sh或sz前缀),以及企业微信API的权限是否正确。此外,处理可能的异常,比如网络错误、数据解析失败等,确保程序不会因为临时错误而崩溃。 最后,生成相关问题,帮助用户扩展知识,比如性能优化、多股票监控、其他通知方式(邮件、QQ,如引用[1]提到的),或者数据存储(如引用[4]中的项目使用MongoDB)。 需要确保所有步骤符合微信的使用政策,避免用户滥用API导致账号被封。同时提醒用户股票API的稳定性和法律合规性,比如使用公开可用的数据源,避免侵犯版权。 总结下来,步骤应该是:选择股票数据源,设置监控逻辑,配置微信通知(企业微信),编写整合代码,处理异常和日志,测试运行,最后考虑扩展和优化。</think>以下是一个基于Python的股票价格监控与微信通知解决方案,整合了实时数据获取、阈值判断和企业微信通知功能: ### 一、技术架构组成 1. **数据获取层**:使用AkShare获取实时股票数据[^4] 2. **业务逻辑层**:通过APScheduler实现定时监控 3. **通知服务层**:企业微信API实现消息推送[^2] ### 二、环境准备 安装所需依赖库: ```bash pip install apscheduler akshare requests ``` ### 三、企业微信配置(关键步骤) 1. 注册企业微信创建应用 2. 记录以下参数: - 企业ID:`wwxxxxxx` - 应用ID:`1000002` - 应用Secret:`xxxxxxxx` ### 四、完整实现代码 ```python import requests import akshare as ak from apscheduler.schedulers.blocking import BlockingScheduler # 企业微信配置 CORP_ID = "wwxxxxxx" APP_ID = "1000002" APP_SECRET = "xxxxxxxx" TOKEN_URL = f"https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={CORP_ID}&corpsecret={APP_SECRET}" SEND_URL = "https://qyapi.weixin.qq.com/cgi-bin/message/send" def get_wechat_token(): response = requests.get(TOKEN_URL).json() return response["access_token"] def send_wechat_msg(content): token = get_wechat_token() payload = { "touser": "@all", "msgtype": "text", "agentid": APP_ID, "text": {"content": content} } requests.post(SEND_URL, params={"access_token": token}, json=payload) def stock_monitor(): try: # 示例监控上证指数 df = ak.stock_zh_a_spot() stock = df[df["代码"] == "sh000001"].iloc[0] current_price = stock["最新价"] if current_price > 3500: msg = f"📈 大盘突破3500点!当前价格:{current_price}" send_wechat_msg(msg) elif current_price < 3000: msg = f"📉 大盘跌破3000点!当前价格:{current_price}" send_wechat_msg(msg) except Exception as e: print(f"监控异常:{str(e)}") if __name__ == "__main__": scheduler = BlockingScheduler() scheduler.add_job(stock_monitor, 'interval', minutes=5) scheduler.start() ``` ### 五、关键参数说明 1. 股票代码对照: - 沪市股票前加`sh` - 深市股票前加`sz` 2. 监控频率参数: - `minutes=5` 表示每5分钟检查一次 - 可调整为`seconds=30`实现半分钟级监控 ### 六、扩展优化建议 1. 多股票监控:使用列表存储多个股票代码 2. 动态阈值:从数据库读取不同股票的预警值 3. 异常处理:增加网络重试机制(可参考Requests库的重试功能[^3]) 4. 日志记录:集成logging模块记录监控历史
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值