痞子达的小站 http://pizida.com/ zh-CN 技术笔记,分享生活 Wed, 23 May 2018 11:57:00 +0000 Wed, 23 May 2018 11:57:00 +0000 小程序速成系列:手把手教你设计用户登录体系 http://pizida.com/miniProgram-login-design.html http://pizida.com/miniProgram-login-design.html Wed, 23 May 2018 11:57:00 +0000 engine_go 跳一跳的出现再次激活了小程序,吃饭在跳,睡觉在跳,洗澡在跳。有句话说得好,谁掌握流量,谁掌握过去;谁掌握入口,谁掌握现在;谁掌握趋势,谁掌握未来,即使我们不能掌握趋势,也要顺应趋势。

小程序内的用户体系

通常要构建一个用户体系需要解决两个关键问题:
1、用户id在系统中保证唯一
2、维护用户在系统中的登录状态

只要这两个问题得到解决,其他事情就是无限的拓展和丰富。比如你要给用户增加昵称、性别、年龄,或者给用户增加角色权限等等,这些都是用户体系下的延伸。盖高楼最重要的是坚固稳定的基石,其他楼层的搭建就是时间问题。

小程序里面也有一套自己的用户体系,所以我们不必从0去开发,可以通过其登录接口进行开发。

以下是小程序官方的登录流程图(先认真看两遍)
小程序的登录流程图

小程序:指的是小程序前端框架,也就是在微信开发者工具中写代码的部分
第三方服务器:指的是我们自己的后端服务器,此处对微信而言是第三方服务器,后面我们统一称为业务服务器
微信服务器:指的是微信的后端服务器,也就是提供的API接口,目前域名地址为 : https://api.weixin.qq.com

微信官方已经给出大致思路:
第一步:通过wx.login的API获取登录凭证code,发送请求到第三方服务器,并带上code,然后调用微信后端API获得openidsession_key.

第二步:生成自己的session的键名,这里把该key称为3rd_session,以3rd_session为key,session_key+openid为value,写入session(session可以通过Redis或Memcached的KV存储),这里的过期时间可以根据业务场景自行设置

第三步:生成3rd_session后,在客户端写入storage,保存在本地,这里最好再机上过期时间expire,每次登录之前判断3rd_session是否过期,如果过期,则重新请求登录,生成3rd_session

第四步:wx.request每次带上3rd_session,向后端发起请求,根据3rd_session在session存储中查找合法的session_keyopenid

静默获取openid

上面第一步提到了微信的wx.login接口,该接口会返回一个临时凭证code,为什么是临时凭证,因为用户每次进入小程序,code都是不同的。

//登录
wx.login({
  success: res => {
    console.log(res)
  }
})

临时凭证code

接下来用code换取openid,
前端代码:

    // 登录
    wx.login({
      success: res => {
        if (res.code) {
          wx.request({
            url: 'https://test.com/test/login.php', //此处仅仅为演示接口
            data: {
              code: res.code
            },
            success: function (res) {
              console.log(res)
            }
          })
        } else {
          console.log('获取code失败' + res.errMsg)        
        }
      }
    })

获取openid的开放接口:

https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code

请求参数

参数必填说明
appid小程序唯一标识
secret小程序的 app secret
js_code登录时获取的 code
grant_type填写为 authorization_code

返回参数

在不满足UnionID下发条件的情况下,返回参数

参数说明
openid用户唯一标识
session_key会话密钥

在满足UnionID下发条件的情况下,返回参数

参数说明
openid用户唯一标识
session_key会话密钥
unionid用户在开放平台的唯一标识符

https://test.com/test/login.php 对应的后端代码:

    public function login()
    {
        $code   =  isset($_POST['code']) ? $_POST['code'] : "";
        $appid  = "你的小程序id";
        $secret = "你的小程序密钥";  //密钥和appid都在微信公众后台生成
        //这里为了演示直接把获取openid的api写在当前方法中,实际项目建议封装所有的微信接口,通过某个库抽象处理
        $openid_api  = "https://api.weixin.qq.com/sns/jscode2session?appid={$appid}&secret={$secret}&js_code={$code}&grant_type=authorization_code";
        return  file_get_contents($openid_api);
    }

前端如图所示,已经获取到openid(在这个小程序中是唯一的),可以为所欲为了。整个过程是静默获取,用户没有感知。
获取openid

业务方维护登录态

根据上面的流程图,可以看到微信建议我们自己维护登录态,生成3rd_session(表示第三方会话)。

关于3rd_session的生成,上面的流程图已经说得非常详细。可以通过Linux操作系统提供的随机数机制,如命令head -n 80 /dev/urandom | tr -dc A-Za-z0-9 | head -c 64,生成出来的就是3rd_session,我们使用它作为key,然后以openid + session_key作为value。

我们可以使用KV存储(Redis或Memcached)这个会话,这里使用Redis。

    public function create3rdSession($openid = "", $session_key = "")
    {
        if (!$openid || !$session_key) {
            return false;
        }
        //通过操作系统随机数机制生成key
        exec('head -n 80 /dev/urandom | tr -dc A-Za-z0-9 | head -c 64', $exec_ret);
        if (! isset($exec_ret[0]) || strlen($exec_ret[0]) != 64) {
            return false;
        }
        $app_session_key = $exec_ret[0]; //随机数作为会话的key
        $app_session_value = ['openid'=>$openid, 'session_key'=>$session_key]; //openid和微信的session_key作为会话的value
        cache($app_session_key, $app_session_value, 3600);//会话缓存1小时
        return $app_session_key;
    }

上面的create3rdSession产生了会话,但是其中两个参数是需要接口获得,那么我们在login方法里面丰富一下:

   public function login()
    {
        $code = isset($_POST['code']) ? $_POST['code'] : "";
        $ret = array();
        $appid = "你的小程序id";
        $secret = "你的小程序密钥";
        $openid_api  = "https://api.weixin.qq.com/sns/jscode2session?appid={$appid}&secret={$secret}&js_code={$code}&grant_type=authorization_code";
        $result = file_get_contents($openid_api);
        $ret = json_decode($result, 1);
        //这个地方创建会话,生成一个会话token
        $token = $this->create3rdSession($ret['openid'], $ret['session_key']);
        $ret['token'] = $token;
        return json_encode($ret);
    }

我们再次访问小程序,app.js入口就会调用login的api,返回如下:
创建会话token

再查看下Redis中存储的状态:
会话存储在Redis中

可以看到我们的token是一串随机数,这里我用right_做了前缀,查看该key,可以看见序列化的openid和session_key。ok。至此数据存储成功。

维护登录态已经完成一半,接下来是用户从小程序进入,需要判断是否已经登录。

我们需要把前面生成的token存储到cookie中,然而小程序中并没有cookie机制,需要使用storage来替代。不熟悉的可以看文档:https://developers.weixin.qq.com/miniprogram/dev/api/data.html#wxsetstorageobject

我们通过wx.setStorageSync把token存储在小程序本地,再通过wx.getStorageSync把token取出来,请求接口在header带上这个cookie即可。

前端代码:

    // 登录
    wx.login({
      success: res => {
        if (res.code) {
          wx.request({
            url: 'https://test.com/test/login.php',
            data: {
              code: res.code
            },
            method:"post",
            header: { 
              "Content-Type": "application/x-www-form-urlencoded",
              'cookie': wx.getStorageSync("sessionid") //读取cookie
            },
            success: function (res) {
              let data = res.data
              if (data.status == 1) {
                wx.showModal({
                  title: '提示',
                  content: '已经登录',
                  showCancel:false
                })
              } else if (data.status == 0) {
                wx.showModal({
                  title: '提示',
                  content: '本地存储成功',
                  showCancel: false
                })
                wx.setStorageSync("sessionid", data.token) //设置cookie
              }
            }
          })
        } else {
          console.log('获取code失败' + res.errMsg)        
        }
      }
    })

login接口我们判断如果cookie中带有sessionid就,通过sessionid作为key,去Redis中查找信息。如果有则会话未过期,如果没有则证明已过期,重新读取openid接口。代码如下:

    public function login()
    {
        $ret = array();
        $request = new Request();
        $session_id  = $request->header('Cookie');
        $info = cache($session_id);
        if ($info) {
            $ret['status'] = 1;
            $ret['data'] = $info;
            return json_encode($ret);
        }
        $code = isset($_POST['code']) ? $_POST['code'] : "";
        $appid = "你的小程序id";
        $secret = "你的小程序密钥";
        $openid_api  = "https://api.weixin.qq.com/sns/jscode2session?appid={$appid}&secret={$secret}&js_code={$code}&grant_type=authorization_code";
        $result = file_get_contents($openid_api);
        $ret = json_decode($result, 1);
        //创建会话
        $token = $this->create3rdSession($ret['openid'], $ret['session_key']);
        $ret['status'] = 0;
        $ret['token'] = $token;
        return json_encode($ret);
    }

第一次进入小程序,效果如下:

数据存储成功

控制台成功将token写入到本地:

控制台显示的sessionid

当我们再次进入小程序,效果如下:

已有登录态

header里面也塞上了Cookie,如下:

header中的Cookie

到此,一个简单但完整的登录态已经完成。

谈谈session_key和wx.checkSession()

session_key是通过获取openid的那个接口一起得到的,即接口https://api.weixin.qq.com/sns/jscode2session

但是上面我们的登录态并没有使用,session_key保证了当前用户进行会话操作的有效性,这个session_key是微信服务端给我们派发的。

由于业务自身维护了登态,这里就不没必要使用session_key了,那么是否它一无是处呢?并不是,在获取某些敏感接口需要用到session_key。有两个地方需要:

  • 校验用户信息(wx.getUserInfo(OBJECT)返回的signature);
  • 解密(wx.getUserInfo(OBJECT)返回的encryptedData);

session_key还有两个注意点:

  • session_key和微信派发的code是一一对应的,同一code只能换取一次session_key。每次调用wx.login(),都会下发一个新的code和对应的session_key,为了保证用户体验和登录态的有效性,开发者需要清楚用户需要重新登录时才去调用wx.login()
  • session_key是有时效性的,即便是不调用wx.login,session_key也会过期,过期时间跟用户使用小程序的频率成正相关,但具体的时间长短开发者和用户都是获取不到的

从上述而看,本人不建议使用session_key去维护业务的登录态。

看得仔细,微信小程序文档上还有一个wx.checkSession(),这个API是根据session_key的过期与否来检测当前的会话是否过期。那么我们如果不实用session_key,这个wx.checkSession()也可以不用。实际上前面我们的token已经可以知道会话什么时候过期,这个就显得没什么用处,再者,这个过期时长是微信维护的,业务方也无法控制,在做一些特性逻辑的时候,也不好开发。

划重点,个人建议在维护小程序登录态的时候可以不使用session_keywx.checkSession()这两个东西,感觉很鸡肋,还容易把开发者搞糊涂。

会话过期与注销

会话过期

实际上前面已经有加上过期时间,会话为一个小时,这就是KV的特性的,十分便于做一些过期的事情。

   cache($app_session_key, $app_session_value, 3600);

查看生命周期,只要3600秒过去,会话自动删除,headers中的Cookie就无法读取到Redis中的信息,那就过期了。下图的会话还剩900多秒

会话注销

这个太好做了,提供一个logout的接口,直接把key删除即可。一行代码搞定,不展开了。

cache($app_session_key, null);

关于小程序登录的思考

实际上在小程序中没有注册的概念,因为用户在使用小程序前必须登录微信,此刻实际上已经有存在登录态了,微信让开发者通过小程序的appid和secert去获得openid,这里更确切的叫绑定,而不是注册。每个小程序应该有自己的用户信息,比如需要获得用户的手机号、用户的邮箱等等,这些在初次登录的时候和openid绑定即可,再建立自己的用户表。这样,以后每次进入小程序可以获得用户的其他的相关信息。当然,有些开发者如果完全不使用微信登录这一套,必须有自己的用户名和密码也完全可以自行设计,至于绑不绑openid,那就具体问题,具体分析了。

]]>
2 http://pizida.com/miniProgram-login-design.html#comments http://pizida.com/feed/miniProgram-login-design.html
大年三十前夕用户吐槽活动很卡 http://pizida.com/website-is-slow.html http://pizida.com/website-is-slow.html Thu, 01 Mar 2018 14:03:00 +0000 engine_go 大年三十前夕,用户反馈线上活动页面访问很慢,体验较差。

要放假了,但我心里没慌,观察活动页面,加载的确很卡,极其不流畅,但Web资源依稀可load出来。

这个场景太熟悉了,毕竟以前遇到过,心里基本有底,直觉告诉我是DB问题,但还是按自己排查习惯来。

记录下整个排查过程:

Ping

首先ping下域名,延迟正常,没有丢包情况,同时页面能访问,证明webserver也是ok的,nginx解析html这一部至少没问题。

结论:nginx没挂,DNS解析正常

php-fpm和nignx排查

检查XHR接口,可以看到有接口耗时20s+,这显然是有问题。xhr接口

此时,假设是否HTTP接口在处理的时候压力过大,但上线前经过多次压力测试,QPS可以承受2000+,查看此刻接口调用QPS在50以内,很低,远远没达到瓶颈。

检查web机器,cpu总占用率不到20%。php-fpm和nginx资源占用率也非常低,php-fpm进程cpu占用率最大不过1%,证明php-fpm处理能力没有问题,而且我特地使用static模式,拉起足够多的php-fpm进程应对。

结论:php-fpm和nginx、web机器也hold得住

代码排查

既然xhr发现接口有执行过长的问题,在程序中埋点,记录每个分支逻辑的执行时长,打印log分析。这一步比较枯燥,不过有收获,在一次次的试探中,发现每次执行和DB相关的逻辑,时间较长。难道DB会hold不住?但即使hold不住,也该有cache层挡着。

结论:DB可能hold不住

Sql排查

既然怀疑DB,那么选取线上一条执行频率最高的语句check一下:

select * from table where user_id = xxx and other1 = 'xxx' and other2 = 'xxx'

分析该sql语句,有很大问题,如下所示:

explain  select * from table where user_id = xxx and other1 = 'xxx' and other2 = 'xxx'

sql分析
它的执行效率贼差,这条sql查询几秒才出结果,而type居然是个ALL,这是不科学的,user_id、other1、other2都加了复合索引,单表数据弄到几百万也测过,是毫秒内拉取。

由上可知复合索引肯定没有生效,再三定位,终于发现原因,请看如下sql:

explain  select * from table where user_id = 'xxx' and other1 = 'xxx' and other2 = 'xxx'

sql分析2

我们可以看见这次的分析,type为const,这就非常优秀了,最多只返回一行数据, const 查询速度非常快, 因为它仅仅读取一次即可。但这两条sql有什么不同呢?!没错!就是一个通过string类型查询,一个是通过int类型查询,整型的时候复合索引是完全没生效的,因为user_id本身是string类型。

于是乎,把代码改了,噼里啪啦发布。一打开网页,瞬间不卡了。

以为事情就此结束?NO,毕竟要过年,没那么简单。

结论:索引没有生效,查询效率低下

DB问题

第二天下午,用户仍然反馈活动很卡,我抽取其中一个接口,获得一个关键信息:页面的nginx报502

php-fpm没问题,但会不会是连接DB后一直没响应呢,造成php-fpm无法返回,nginx不知道怎么办,都说是自己的错。

这一次,决定去MySql服务器上查查,果然还有问题:
请输入图片描述

可以看到MySql磁盘IO很高,就是下午3点多的时候。此时看下慢查询,不查则已,一查惊人。在其他业务下,连接同一个DB server,慢查询达到了20秒以上的sql非常多。其中一个日志文件就有12467条慢查询,并且查询时间在几十秒以上的也有几百条,查询效率可想而知。

慢查询记录数
解决方法把所有慢查询的sql优化掉,该删删,该改改。

结论:慢查询过多问题暴露。初步怀疑慢查询导致Mysql服务器cpu占用率过高和IO负载过高,造成Mysql没有正常返回给php,php-fpm无法返回结果,因此nginx一直等待,无法响应,报502 Bad Gateway。

Cache排查

前面提到,即使DB被穿透,但穿透前理论上cache能hold住,redis的扛并发能力是耳熟能详的,压测QPS也是过万,应对活动高峰时期在可控范围内。

于是乎又在代码中乏味地调试,一行一行地定位。终于,在夜深人静的晚上,我发现业务逻辑存在一个bug:
数据写入cache后,cache会重置,相当于每次写入cache就丢失,所以高峰期一来,用户数据只要变动,redis未生效就会造成缓存雪崩效应,请求全部穿透缓存抵达DB,而DB刚好因为慢查询处理不过来,造成了网页很卡或者接口502。

结论:cache层被穿透,缓存雪崩

优化前和优化后效果对比

优化后,MySql服务器的cpu使用率在高峰期也大大降低,几乎还有空闲的情况(业务逻辑中DB操作很少),而在7号没有优化前使用率最高达到94,这也难怪活动页面会很卡。

总结

解决方式:
1、优化sql,使得索引生效
2、修复业务bug,保证cache命中率
3、去掉多余的索引,保证写入效率

排查后,通过上面三种方式彻底解决问题,整个春节过得相对舒心。

]]>
12 http://pizida.com/website-is-slow.html#comments http://pizida.com/feed/website-is-slow.html
博客之旅又开始了 http://pizida.com/open-blog.html http://pizida.com/open-blog.html Sat, 17 Feb 2018 14:20:00 +0000 engine_go 是的,博客之旅又开始了,这几年写文章没有断,最早玩新浪博客,后来转向wordpress,然后又捣腾jekyll,还专门写过相关的jekyll以及github搭建独立博客的指引,总之,折腾过不少,但种种原因让我关掉了之前的博客,部分文章遗失。

为了表明写博决心,这次我备案了。是的,你怎能相信以前那个在footer写着粗言秽语的年轻人备案了。虽然中途关掉博客,但我还在各个平台写写技术文章和笔记,包括知乎、简书、公众号等等,总觉得不写点儿东西就如废了一般。

博客使用的技术和方案:

  1. Web环境为PHP/7.1.5 + nginx/1.13.0 + MariaDB/5.5.52
  2. 博客采用开源系统typecho,主题是https://github.com/siseboy/junichi,目前微调一下,后续可能还会改动;
  3. 程序部署在腾讯云,机型为标准型S1,1核CPU,1GB内存,系统盘为20GB(本地硬盘);
  4. 图片存储服务使用七牛;
  5. 备案(这个才是最重要的技术o(∩_∩)o );

博客搭建起来大约两天,加上工信部审核备案,整个部署时间大概一个月。备案过程中遭遇了一些问题,还好都解决了。
希望这个博客可以坚持下去。

]]>
4 http://pizida.com/open-blog.html#comments http://pizida.com/feed/open-blog.html