分类: 开发编程

  • 本地https快速解决方案 mkcert

    mkcert 是一个使用 go 语言编写的生成本地自签证书的小程序,具有跨平台,使用简单,支持多域名,自动信任 CA 等一系列方便的特性可供本地开发时快速创建 https 环境使用。

    mac 安装

    brew install mkcert

    使用步骤

    1. # mkcert -install

    Created a new local CA 💥
    Sudo password:
    The local CA is now installed in the system trust store! ⚡️


    1. # mkcert xxx.com "*.xxx.com" localhost 127.0.0.1 ::1

    Created a new certificate valid for the following names 📜
    “xxx.com”
    “*.xxx.com”
    “localhost”
    “127.0.0.1”
    “::1”
    Reminder: X.509 wildcards only go one level deep, so this won’t match a.b.xxx.com ℹ️
    The certificate is at “./xxx.com+4.pem” and the key at “./xxx.com+4-key.pem” ✅
    It will expire on 30 November 2025

  • 分布式锁

    1. 为什么需要分布式锁

    随着业务的发展,一个应用可能部署到好几台服务器上,此时若多台机器需要同步访问同一个资源,就需要使用到分布式锁

    2. 锁的实现

    2.1 基于数据库实现

    通过增加递增的版本号字段实现乐观锁:

    线程1: amount=10, version=123

    select amount,version from bank where id = 1

    线程2: amount=10, version=123

    select amount,version from bank where id = 1

    线程1: update=1,更新成功,更新后version为124

    update bank set version = 124, amount = amount-10 where id = 1 and version = 123

    线程2: 由于当前的version为124,update=0,更新失败

    update bank set version = 124, amount = amount-10 where id = 1 and version = 123

    2.2 基于redis实现

    2.2.1 setnx

    SET lockKey randomValue NX PX 30000

    实现思路:

    1. 获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID
    2. 释放锁的时候,通过UUID判断是不是该锁,若是该锁,则释放锁。
    try {
        lock = redisTemplate.opsForValue().setIfAbsent(lockKey, UUID);
        if (lock) {
            //成功 设置过期时间
            redisTemplate.expire(lockKey,1, TimeUnit.MINUTES); 
            // TODO
        }else {
            //没有获取到锁
        }
    } finally {
        if(lock){   
            //任务结束,释放锁  
            redisTemplate.delete(lockKey);
        }
    }
    

    要点

    1. 为什么锁要添加一个超时时间?如果A获取了锁,宕机了没有释放锁,会造成死锁。
    2. 锁的value值为UUID,是为了避免这种情况:假设A获取了锁,过期时间10s,此时15s之后,锁已经自动释放了,A去释放锁,但是此时可能B获取了锁。A就不能删除B的锁了。

    还有一些可以完善的地方:如果在第一步setnx执行成功后,在expire()命令执行成功前,发生了宕机的现象,那么就会出现死锁的问题

    如果考虑redis的部署问题

    • 单机模式:只要redis故障了,就不能加锁
    • master-slave + sentinel选举模式:如果master节点故障了,发生主从切换,就有可能出现锁丢失的问题。
    • redis cluster模式

    2.2.2 RedLock

    redis的作者也考虑到上面的问题,提出了一个RedLock的算法:假设redis的部署模式是redis cluster,总共有5个master节点,通过以下步骤获取一把锁:

    • 获取当前时间戳,单位是毫秒
    • 轮流尝试在每个master节点上创建锁,过期时间设置较短,一般就几十毫秒
    • 尝试在大多数节点上建立一个锁,比如5个节点就要求是3个节点(n / 2 +1)
    • 客户端计算建立好锁的时间,如果建立锁的时间小于超时时间,就算建立成功了
    • 要是锁建立失败了,那么就依次删除这个锁
    • 只要别人建立了一把分布式锁,你就得不断轮询去尝试获取锁

    但是这样的这种算法还是颇具争议的,可能还会存在不少的问题,无法保证加锁的过程一定正确。

    争议

    1. 官方的推荐 https://redis.io/topics/distlock

    2. Martin Kleppmann 关于 Readlock 的评价。
      https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html

    3. redis 作者的回复。
      http://antirez.com/news/101

    2.2.3 Redisson

    Redisson是一个企业级的开源Redis Client,也提供了分布式锁的支持。

    实现1设置了超时时间,但是超过时间都还没有完成业务逻辑的情况下,key会过期,其他线程有可能会获取到锁。这样一来的话,第一个线程还没执行完业务逻辑,第二个线程进来了也会出现线程安全问题。所以我们还需要额外的去维护这个过期时间,Redisson帮我们做了这些。

    实现细节:
    Redisson所有指令都通过lua脚本执行,redis支持lua脚本原子性执行
    Redisson设置一个key的默认过期时间为30s,如果某个客户端持有一个锁超过了30s,Redisson中有一个watchdog的概念,翻译过来叫看门狗,它会在你获取锁之后,每隔10秒帮你把key的超时时间设为30s。这样就算一直持有锁也不会出现key过期,其他线程获取到锁的问题了。
    – “看门狗”逻辑保证了没有死锁发生。如果机器宕机了,看门狗也就没了。此时就不会延长key的过期时间,到了30s之后就会自动过期,其他线程可以获取到锁

    另外,Redisson还提供了对redlock算法的支持

    2.3 基于zookeeper实现

    zookeeper是一个为分布式应用提供一致性服务的开源组件, 为我们提供了高可用、高性能、稳定的分布式数据一致性解决方案。它内部是一个分层的文件系统目录树结构,规定同一个目录下只能有一个唯一文件名,利用这个特性来实现锁:

    ​ 让多个客户端同时创建一个临时节点,创建成功的就说明获取到了锁 。然后没有获取到锁的客户端创建一个 watcher 进行节点状态的监听,如果这个互斥锁被释放了,可以调用回调函数重新获得锁。zk 中不需要向 redis 那样考虑锁得不到释放的问题,因为当客户端挂了,节点也挂了,锁也释放了。

    如何同时实现 共享锁和独占锁 ?创建有序的临时节点。

    1. 当读请求(获取共享锁),如果 没有比自己更小的节点,或比自己小的节点都是读请求 ,则可以获取到读锁。如果比自己小的节点中有写请求 ,则只能等待前面的写请求完成。
    2. 当写请求(获取独占锁),如果 没有比自己更小的节点 ,则表示当前客户端可以直接获取到写锁。如果 有比自己更小的节点,无论是读操作还是写操作,当前客户端都无法获取到写锁 ,等待所有前面的操作完成。

    当然还有优化的地方,比如当一个锁得到释放它会通知所有等待的客户端从而造成 惊群效应 。此时你可以通过让等待的节点只监听他们前面的节点,让 读请求监听比自己小的最后一个写请求节点,写请求只监听比自己小的最后一个节点

    2.4 锁对比

    实现 优点 缺点
    数据库 简单,易于理解,系统依赖少 1. 性能较差,有锁表的风险 2. 非阻塞,需要轮询,占用CPU资源
    redis 性能很高,可以支撑高并发的获取、释放锁操作 1. 数据并不是强一致性的,在某些极端情况下,可能会出现问题 2. 锁删除失败,过期时间不好控制 2. 非阻塞,需要轮询,占用CPU资源
    zk 1. 简单易用,有较好的性能和可靠性 2. 不用一直轮询,性能消耗较小 3.可解决失效死锁问题 1. 性能不如redis实现,需要动态创建、销毁临时节点,且只能通过 Leader 服务器来执行,然后将数据同步到所有的 Follower 机器

    3. 分布式锁的要点

    3.1 AP 还是 CP

    3.1.1 AP模型

    ​ 例子:Redis的主备集群做例子

    好处:

    ​ 在接受事务请求(增删改数据)的时候,主Master节点只需要确保自己写入即可立即返回给客户端,复制的过程由于是异步的,客户端延时性上来说影响并不大,相比于CP模型的确保半数提交成功,AP模型的延时性是比较低的Redis本身的定位就是要快,所以这相当符合Redis的设计初衷,如果集群有三个节点,他可以容许宕机两个节点,可以看出来,可用性的容错节点是N-1个,相比于CP模型他的可用性会更高

    Redis作为分布式锁的话有可能会造成数据的不一致,如果你使用分布式锁的场景是为了更好的利用系统资源(CPU、内存),让多节点不做一些重复的工作,并行互斥执行不同的任务,那么不妨将任务做成幂等,这样就算两个节点做同一个任务,任务被执行了两次但是它们是幂等的,其结果也不会被影响

    3.1.2 CP模型

    ​ 例子:zookeeperZab协议)

    存在的问题:

    ​ 半数以上的节点上确认,延时性就取决于最快的那半数节点的写入性能,而且多加了网络通信来回的开销。微服务链路调用都需要注册中心获取服务的IP地址,并发量大,但IP地址这种东西小概率存在不一致(服务刚上线,但注册中心没有这个服务的IP地址,使得不被访问到)其实是可以接受的。在注册中心的场景下延时性才最重要,这也就是为什么Nacos的注册中心会选用AP模型,他的延时性相对是要好的。

    3.1.3 总结

    • 在延迟性要求高、客户端响应不能太慢、性能要求高的场景下,允许牺牲小部分时间的锁失效来换取好的性能,那么建议使用AP模型来实现分布式锁,某些场景可以通过幂等弥补小部分锁失效带来的负面影响
    • 在延迟性要求不高、主要保证锁不能失效、高一致性的场景下,允许牺牲一点性能来换取一致性,那么建议使用CP模型来实现分布式锁。选型时注意考虑到并发量极高的情况下可能有问题

    3.2 宕机锁释放问题

    分布式锁拿到锁的节点意外宕机,拿到锁而不释放锁,从而死锁,这就是一个宕机锁释放问题。

    3.2.1 Redis宕机锁释放问题

    redis中我们解决宕机锁释放问题通常会在设置锁的同时给他设置一个超时时间,这就有一个问题了,这个超时时间要设置多长?如果这个超时时间太长,那节点宕机没释放锁就只能等待锁超时,死锁时间会变长(服务至少有一段时间的不可用),这是不容许的,那如果超时时间太短,又会造成如果有什么做了很久的业务操作,这边还没执行完,另一个节点却也能获取到锁,造成的锁失效。

    Redisson其实解决了超时时间过短,锁失效的问题,虽然有续租,但是不建议超时时间太长,如果超时时间太长还是会造成死锁的时间(如果超时时间设置1小时。。那还是会有1小时锁无法获取的情况),也不建议太短,万一JVM进行GC,整个代码进行停顿,后台线程因此有几秒时间无法续租,锁也会失效被其他节点获取,所以这里建议超时时间设置的不大偏小,3、5分钟左右这样子

    3.2.2 zookeeper的宕机锁释放问题

    使用zookeeper作为分布式锁,客户端会在zk上创建一个临时节点,获取到锁的客户端会与zk维持一个心跳连接,如果zk收不到客户端的心跳就说明客户端宕机了,此时临时节点会自动释放,相当于自动释放了锁

    3.3 锁等待问题

    A节点获取到锁执行锁区块的业务逻辑,B节点获取不到锁,那么B节点怎么才能知道自己需要阻塞等待多久?这就需要一个通知机制,在锁释放的时候中间件需要通知等待中的节点来获取锁。

    3.3.1 redis中的锁等待问题

    Redisson利用了PubSub模式完成了一个锁释放的通知机制

    1. 利用redisPubSub模式订阅一个LockName关联的channel(一把锁对应一个channel
    2. 设置一个监听器,监听PubSub中名称为刚刚的那个LockNamechannel发出通知(有锁释放),动作为调用Semaphorerelease方法释放信号量
    3. 当前获取不到锁的线程调用Semaphoreacquire方法尝试获取信号量,若没有信号量则阻塞ttl个时间
    4. 等待超过ttl个时间或者有锁释放通知之后线程唤醒,继续尝试获取锁
    5. 若获取不到锁,继续调用Semaphore#acquire方法阻塞然后获取锁,无限循环直到获取到锁

    3.3.2 zookeeper中的锁等待问题

    同样使用通知机制(观察者模式)会比较好解决,在zookeeper中方案就是Watch机制,监听一个节点是否产生变化,若变化会收到一个通知,当获取不到锁之后监听锁的那个临时节点即可。也可以按顺序来,获取锁失败之后注册一个顺序节点,按照自己的顺序,向前一个节点注册Watch,这样一个个来可解决惊群效应。

    3.4 误释放锁

    redis作为分布式锁误释放锁

    1. A节点获取到锁之后,redis挂了,重新选举一个从节点的redis后由于是AP模型,锁信息不在这个从节点上,B节点此时来获取锁成功,B开始执行业务逻辑,A执行完业务逻辑之后来释放锁,就会把B的锁释放掉了…然后C又来获取锁,B执行完又把C的锁释放掉…以此类推

    2. A节点获取到锁之后,因为某种原因(GC停顿或者…)没有续租过期时间,锁不小心释放掉了但是业务逻辑还在跑,B节点此时来获取锁成功,B也在跑业务逻辑,A执行完逻辑之后释放锁,把B的锁也给释放掉了…然后C又来获取锁,B执行完又把C的锁释放掉…以此类推

    zookeeper有误释放锁的情况

    1. 假设A节点获取到锁,此时GC停顿,后台线程无法给zookeeper发送心跳,zk以为A节点宕机,把临时节点给删了,这样其他节点也会乘虚而入,然后就会出现上面说的循环释放别人锁的情况。

    3.4.1 解决方案

    借鉴Redisson的方案,在获取锁的时候将一个唯一标识设置为value(UUID+threadId),设置一个threadId还有一个好处,就是可以做可重入锁,当同一个线程再次获取锁的时候就可以以当前threadId作为依据判断是否是重入情况。

  • Linux下安装Go环境

    Golang官网下载地址:https://golang.org/dl/
    1. 打开官网下载地址选择对应的系统版本, 复制下载链接
    wget https://dl.google.com/go/go1.15.6.linux-amd64.tar.gz
    2. tar解压到/usr/loacl目录下,得到go文件夹
    tar -C /usr/local -zxvf go1.15.6.linux-amd64.tar.gz
    3. 添加/usr/loacl/go/bin目录到PATH变量中。添加到/etc/profile$HOME/.profile都可以
    vi /etc/profile
    // 在最后一行添加
    export GOROOT=/usr/local/go
    export PATH=$PATH:$GOROOT/bin
    // :wq保存退出后source一下
    source /etc/profile
    4. 执行go version,如果显示版本号,则安装成功。

  • REST API 设计最佳实践

    一 前言

    作为一名后端程序员,照着产品需求设计好了模型,设计好了关联关系,设计API时候问题来了:一旦 API 进入前端 APP 代码,或者是被你的顾客广泛使用的话,再来大改就非常麻烦了。比如说,如果 APP 版本 1.0 用了一个接口 A,这个接口 A 如果要进行大改,那么必须将 A 维持至所有用户升级过 APP 1.0 后。那么怎么样避免 API 发布之后大改呢?有没有一些提前可以注意到的设计准则可以帮我们避开 API 设计中的各种坑?

    二 REST API 是什么

    REST API 有一套 API 设计的准则,它规范了 API 设计的框架,使得服务间、程序员之间有一个通用的沟通语言。

    三 REST API 内具体规定了什么

    REST API 规范了 API 设计的两大核心原则

    1. API应该作用于 Resource(资源)上
    2. 对资源的操作应使用对应语义的几种操作,包括: GET, POST, PUT, PATCH, DELETE

    什么是Resource(资源)

    这里的资源是可操作的逻辑对象,允许调用者进行操作,比如用户注册,那么 API 类似于POST /users,资源即为 users。在很多情况下,API 中的资源与你的数据模型是一一对应的。当然也有例外情况,比如说你的数据库中存有用户,但是你现在想要让调用者可以创建“管理员”,那么 API 可能是POST /admins表中并没有 admins 这个表,可能 admin 是 Users 表中的一个属性,比如 role=admin
    REST API中的资源一定需要是名词,即一定是一个实在存在的概念,比如 用户, 帐号, 车票等,或一个抽象的概念,比如 权限 等。
    如果你需要提供一个创建某种资源的API接口,POST /indexesPOST /accountsPOST /docs等等。
    对于资源的命名,建议统一命名为为英文的复数。比如说 users 而不是 user。更重要的是保持一致性,在所有地方用一样的复数。

    什么是操作

    一旦定义了资源,接下来需要定义允许调用者在这些资源上做什么操作。
    比如说,以抢车票为例,我们可能允许调用者进行以下操作

    • GET /tickets – 列出所有车票
    • GET /tickets/9839 – 列出 id 为 9839 这张车票的信息
    • POST /tickets – 创建一张车票
    • PUT /tickets/9839 – 更新 9839 这张车票的信息
    • PATCH /tickets/9839 – 部分修改 983 这张车票的信息,比如只修改车票价格
    • DELETE /tickets/9839 – 删掉 9839 这张车票

    以上可以总结出来REST的大致设计思路了。它由两部分组成,第一部分是 操作,第二部分是可操作的 资源。比如上文中的 GET /tickets,操作是 GET,可操作的资源是车票。
    如果严格遵循了REST的设计准则,调用者也了解 REST 的准则的话,那么对于很多 API 调用,不用再参考互相写的文档了。如果需要调用一张车票的信息,调用者自然会知道应该用GET去查看一个车票资源的信息,即 GET /tickets/:ticketId,这样就极大降低了沟通成本和出错成本,提升效率。

    如何在 API 中表示实体(数据库表)间关系

    在后端设计中,有的资源逻辑上无法独立存在。比如说,有索引存在,用GET /indexes/index_abc/docs/1来表达获取索引 index_abc 中编号为 1 的文档。因此,对于所有资源需要依赖于另一个资源存在时,我们就按顺序在端点中将资源列出来。索引和文档的关系,可以有以下接口

    • GET /indexes/index_abc/docs/1 – 获取index id为 index_abc 下的id为 1 的文档
    • GET /indexes/index_abc/docs – 获取index id为 index_abc 下的所有文档
    • POST /indexes/index_abc/docs – 在index id为 index_abc 的索引中,添加文档 …

    如果一个资源可以独立于另一个资源存在,那么可以考虑直接提供子端点。比如说,如果一个宠物店主人和宠物信息分别都常常被同时调用,那么可以考虑

    GET /owners/  – 获取所有主人信息
    GET /owners/1/pets/ 获取 id 为 1 的主人的所有宠物

    GET /pets/ – 获取所有宠物信息(宠物店所有宠物)

    GET /pets/13 – 直接获取 id 为 13 的宠物

    REST API中如何表示一个动作

    有时候表达一些接口时,会发现REST的准则很难直接应用。比如用户登录POST /users/signin,这里的 signin是个动词。采用REST准则时可以考虑有三个选择

    1. 严格地遵循 REST 原则,找一个替代动词的名词。 signin 可以替换为login。或者,以 token 密钥的方式登录的话,可以改为 POST /users/token,创建一个 user token(也就是登录了)
    2. 在某些实在困难的地方,放弃严格的REST原则
    3. 参考一些成功的 REST API 并寻找类似的 API,参考他们的命名设计,如 github 的 API,较为规范,覆盖了很多 API 调用的情景,可以找到个类似的命名参考。

    比如说,在 github 上,如果让你来设计加星这个操作,你会把端点被设计成什么样?
    Github把加星端点设计为 PUT /gists/:id/star,把取消加星设计为 DELETE /gitsts/:id/star。这样就完美地遵循了 REST 名词作为资源的准则,把动词”加星”完美地用 PUT/DELETE 两个操作清晰地表达出来。

    REST API 设计常见问题和建议

    如何区分版本

    比如说,如果在大致将 v1 开发完毕后,v1 前缀的 API 就应该稳定下来,所有的改动进入 v2。同时开始通知所有使用 v1 的用户,帮助他们平滑迁移到 v2。带有版本前缀的 API 示例如下

    GET /v1/indexes/
    GET /v1/indexes/abc/
    POST /v1/indexes/

    也可以用子域名表示

    http://api.example.com/v1
    http://apiv1.example.com

    或者添加自定义请求头部

    Accept-version: v1
    Accept-version: v2

    过滤、排序、字段选择和分页

    过滤:为所有字段或者查询语句提供独立的查询参数。

    GET /cars?color=red 返回红色车的列表
    GET /cars?seats<=2 返回两座车的列表

    排序:允许跨越多字段的正序或者倒序排列。

    GET /cars?sort=-manufactorer,+model 返回排序的车辆列表

    字段选择:只列出需要的字段。一些情况下,只需要在列表中查询几个有标识意义的字段,并不需要从服务端把所有字段的值都请求出来,因此需要API支持选择查询字段的能力。这样也可以在一定程度上提高网络传输的性能与响应速度。

    GET /cars?fields=manufacturer,model,id,color 返回需要的车辆信息字段的列表

    分页:使用offset和limit来获取固定数量的资源结果,当其中一个参数没有出现时,应该提供各自的默认值,比如默认取第一页,或者默认取20条数据。

    GET /cars?offset=10&limit=5 返回第10页的5条车辆记录列表
    GET /cars?&limit=5 返回前5条车辆记录列表
    GET /cars?&offset=5 返回第5页的车辆记录列表(默认单页数量)

    应该返回什么

    建议 REST API 永远返回 JSON 格式的结果。
    因为这样才能返回标准的结构化数据。所以,服务器回应的 HTTP 头的Content-Type属性要设为application/json。客户端请求时,也要明确告诉服务器,可以接受 JSON 格式,即请求的 HTTP 头的ACCEPT属性也要设成application/json

    HTTP 状态码

    • 1xx:相关信息
    • 2xx:操作成功
    • 3xx:重定向
    • 4xx:客户端错误
    • 5xx:服务器错误

    这五大类总共包含100多种状态码,覆盖了绝大部分可能遇到的情况。每一种状态码都有标准的(或者约定的)解释,客户端只需查看状态码,就可以判断出发生了什么情况,所以服务器应该返回尽可能精确的状态码。
    2xx 状态码
    200状态码表示操作成功,但是不同的方法可以返回更精确的状态码。

    GET: 200 OK
    POST: 201 Created
    PUT: 200 OK
    PATCH: 200 OK
    DELETE: 204 No Content

    上面代码中,POST返回201状态码,表示生成了新的资源;DELETE返回204状态码,表示资源已经不存在。
    此外,202 Accepted状态码表示服务器已经收到请求,但还未进行处理,会在未来再处理,通常用于异步操作。

    参考

    1. https://stackoverflow.blog/2020/03/02/best-practices-for-rest-api-design/
    2. https://hackernoon.com/restful-api-designing-guidelines-the-best-practices-60e1d954e7c9
    3. https://www.vinaysahni.com/best-practices-for-a-pragmatic-restful-api
    4. Azure ( https://docs.microsoft.com/zh-cn/azure/architecture/best-practices/api-design
    5. Google Cloud ( https://cloud.google.com/apis/design/
    6. Zalendo : https://opensource.zalando.com/restful-api-guidelines/ ,其中有非常多的实践是可以参考的,也像 RFC 一样规范了 MUST 、SHOULD 、MAY 的遵守分级。
  • linux磁盘清理后不释放空间还是100%

    通过df -h查看磁盘满了 ,删除文件有,空间没有被立即释放, 通过查阅相关资料,了解到被删除文件被执行rm命令时,如果有进程操作该文件,该文件不会被立马删除,而是被标记为deleted;直到操作该文件的所有进程都结束,该文件才会被删除。 delete状态下的文件不可见,使用ll命令时也看不到,但实实在在占用了磁盘空间。可以通过执行下列命令查看被标记为delete的文件清单:

    lsof | grep deleted

    解决办法:kill -9 PID   把进程删掉就能释放空间。

  • restful API

    说明

    1. 在RESTful架构中,每个网址代表一种资源(resource),所以网址中不能有动词,只能有名词,而且所用的名词一般与数据库的表格名对应。

    2.对于资源的具体操作类型,由HTTP动词表示。常用的HTTP动词有下面五个(括号里是对应的SQL命令)。

    GET(SELECT):从服务器取出资源(一项或多项)。

    POST(CREATE):在服务器新建一个资源。

    PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。

    PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)。

    DELETE(DELETE):从服务器删除资源。

    3. 返回结果,针对不同操作,服务器向用户返回的结果应该符合以下规范。

    GET /collection:返回资源对象的列表(数组)

    GET /collection/resource:返回单个资源对象

    POST /collection:返回新生成的资源对象

    PUT /collection/resource:返回完整的资源对象

    PATCH /collection/resource:返回完整的资源对象

    DELETE /collection/resource:返回一个空文档

    实现(示例)

    请求路径方法对应方法名如下

    请求方法 请求路径 访问控制器方法 说明

    GET /user index() 返回用户列表

    GET /user/123 show($id) 返回$id=123的用户信息

    POST /user store() 新建一个用户

    PUT / PATCH /user/123 update($id) 修改$id=123的用户信息

    DELETE /user/123 delete($id) 删除$id=123的用户

    POST /user/foo foo() 自定义方法

    其他

    前端请求方法为PUT / PATCH/DELETE 时,属于非简单访问,会产生跨域问题,会在正式通信之前,增加一次HTTP查询请求,预请求用的请求方法是OPTIONS。
    服务器收到”预检”请求以后,检查Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段,确认允许跨源请求,就可以做出回应:

    Access-Control-Allow-Origin: *
    Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE
    Access-Control-Allow-Headers: Origin,X-requested-with,content-Type,Accept

  • axios 参数为payload的解决方法

    1. 添加头部headers

      // 新创建 axios 实例配置
      const $axios = axios.create({
      baseURL: ‘http://domain.com’,
      timeout: 5000,
      headers: {
      ‘Content-Type’: ‘application/x-www-form-urlencoded’,
      ‘sessionId’: Lockr.get(“sessionId”),
      ‘authKey’: Lockr.get(“authKey”),
      }
      });

    2. 参数序列化

      var qs = require(‘qs’);
      axios.post(‘/foo’, qs.stringify({ ‘foo’: ‘bar’ });

    见 https://github.com/mzabriskie/axios/blob/master/README.md#using-applicationx-www-form-urlencoded-format

  • PhpStorm配置Xdebug调试

    xdebug 安装

    打开 https://xdebug.org/wizard.php 复制phpinfo的信息到文本框,会返回安装指导:

    Tailored Installation Instructions
    
    Summary
    
    Xdebug installed: no
    Server API: FPM/FastCGI
    Windows: no
    Zend Server: no
    PHP Version: 5.6.9
    Zend API nr: 220131226
    PHP API nr: 20131226
    Debug Build: no
    Thread Safe Build: no
    Configuration File Path: /usr/local/php/etc
    Configuration File: /usr/local/php/etc/php.ini
    Extensions directory: /usr/local/php/lib/php/extensions/no-debug-non-zts-20131226
    Instructions
    
    Download xdebug-2.5.1.tgz
    Unpack the downloaded file with tar -xvzf xdebug-2.5.1.tgz
    Run: cd xdebug-2.5.1
    Run: phpize (See the FAQ if you don't have phpize.
    
    As part of its output it should show:
    
    Configuring for:
    ...
    Zend Module Api No:      20131226
    Zend Extension Api No:   220131226
    If it does not, you are using the wrong phpize. Please follow this FAQ entry and skip the next step.
    
    Run: ./configure
    Run: make
    Run: cp modules/xdebug.so /usr/local/php/lib/php/extensions/no-debug-non-zts-20131226
    Edit /usr/local/php/etc/php.ini and add the line
    zend_extension = /usr/local/php/lib/php/extensions/no-debug-non-zts-20131226/xdebug.so
    Restart the webserver
    If you like Xdebug, and thinks it saves you time and money, please have a look at the donation page.
    

    xdebug 配置

    编辑php.ini文件,加入

    [Xdebug]  
    ;指定Xdebug扩展文件的绝对路径  
    zend_extension = /usr/local/php/lib/php/extensions/no-debug-non-zts-20131226/xdebug.so
    ;允许远程IDE调试
    xdebug.remote_enable        = true
    ;通知 PHP 开启调试的标识
    xdebug.idekey = PHPSTORM
    ;远程主机
    xdebug.remote_host          = 192.168.xxx.xxx
    ;xdebug.remote_port         = 9000 ;默认端口 9000
    

    配置参数选项附录

    配置参数选项 参数值类型与默认值    参数选项描述
    xdebug.auto_trace   boolean类型,默认值=0 是否在脚本运行之前自动调用相关追踪函数。
    xdebug.cli_color    integer类型,默认值=0 该参数自2.2版本开始引入。如果值=1,当处于CLI模式或连接虚拟控制台时,Xdebug将高亮显示var_dumps()和堆栈输出,;在Windows中,这需要安装ANSICON工具。如果值=2,不管是否处于CLI模式或连接虚拟控制台,Xdebug都会高亮显示var_dumps()或堆栈输出;这种情况下,你可能会看到转义后的代码。
    xdebug.collect_assignments  boolean类型,默认值=0 该参数自2.1版本开始引入。用于控制是否为函数跟踪添加变量赋值功能。
    xdebug.collect_includes boolean类型,默认值=1 控制是否在跟踪文件中写入include()、include_once()、require()、require_once()等函数中用到的文件名。
    xdebug.collect_params   integer类型,默认值=0 
    控制在调用函数时,是否收集传递给函数的参数信息。如果参数值过大,这可能会占用大量的内存;不过,在Xdebug 2中不会出现该问题,因为Xdebug 2将相关数据写入磁盘中,而不是占用内存。
    
    如果值=0,则不显示任何信息。
    如果值=1,只显示类型和大小信息,例如:string(6)、array(8)。
    如果值=2,将显示类型和大小,以及全部信息的工具提示。
    如果值=3,将显示变量的全部内容。
    如果值=4,将显示变量的全部内容和变量名。
    
    xdebug.collect_return   boolean类型,默认值=0 控制是否在追踪文件中写入函数调用的返回值。
    xdebug.collect_vars boolean类型,默认值=0 控制是否收集指定作用域中的变量信息。由于需要反向工程PHP的操作码数组,因此Xdebug的分析速度可能比较慢。
    xdebug.coverage_enable  boolean类型,默认值=1 该参数自2.2版本开始引入。控制是否允许通过设置内部结构来启用代码覆盖率功能。
    xdebug.default_enable   boolean类型,默认值=1 当发生异常或错误时,是否默认显示堆栈信息。
    xdebug.dump.*   string类型,默认值=Empty  这里的*可以是COOKIE, FILES, GET, POST, REQUEST, SERVER, SESSION中的任意一个。用于指定发生错误时是否显示超全局变量数组中的索引变量信息。比如,你想要显示请求的IP地址和请求方式,可以设置为
    xdebug.dump.SERVER=REMOTE_ADD,REQUEST_METHOD
    多个索引变量用英文逗号隔开,如果要输出其中的所有变量,可以直接用*,例如:
    xdebug.dump.GET=*
    xdebug.dump_globals boolean类型,默认值=1 控制是否显示通过xdebug.dump.*定义的所有超全局变量的信息。
    xdebug.dump_once    boolean类型,默认值=1 如果出现多个错误,控制超全局变量信息是在所有错误中显示,还是只在第一个错误中显示。
    xdebug.dump_undefined   boolean类型,默认值=1 控制是否显示超全局变量中未定义的值。
    xdebug.extended_info    integer类型,默认值=1 是否强制进入PHP解析器的"extended_info"模式,这将允许Xdebug以远程调试器对文件或行添加断点。开启此模式将拖慢脚本的允许速度,该参数只能在php.ini中设置。
    xdebug.file_link_format string类型,默认值=,  自2.2版本开始引入。用于指定堆栈信息中用到的文件名称的链接样式,这允许IDE通过设置链接协议,直接点击堆栈信息中的文件名称,即可快速打开指定的文件。例如:ZendStudio://%f@%l(%f表示文件路径,%f表示行号)。
    xdebug.force_display_errors integer类型,默认值=0 自2.3版本开始引入。是否强制显示错误信息。
    xdebug.force_error_reporting    integer类型,默认值=0 自2.3版本开始引入。是否强制显示所有错误级别的信息。
    xdebug.halt_level   integer类型,默认值=0 自2.3版本开始引入。指定出现那些错误级别的错误时,中止程序运行。例如:xdebug.halt_level=E_WARNING|E_NOTICE|E_USER_WARNING|E_USER_NOTICE(也仅支持上述4种错误级别)。
    xdebug.idekey   string类型,默认值=*complex*  指定传递给DBGp调试器处理程序的IDE Key。
    xdebug.manual_url   string类型,默认值=http://www.php.net 仅2.2.1以下版本可用,用于指定从函数堆栈和错误信息链接到的帮助手册的基本URL。
    xdebug.max_nesting_level    integer类型,默认值=100   指定递归的嵌套层级数。
    xdebug.overload_var_dump    boolean类型,默认值=1 自2.2版本开始引入,当php.ini中的html_error设为1时,Xdebug是否默认使用自身的改进版本来重载var_dump()。
    xdebug.profiler_append  integer类型,默认值=0 当多个请求映射到相同文件时,指定是覆盖之前的调试信息文件还是追加内容到该文件中。
    xdebug.profiler_enable  integer类型,默认值=0 指定是否启用Xdebug的性能分析,并创建性能信息文件。
    xdebug.profiler_output_dir  string类型,默认值=/tmp   指定性能分析信息文件的输出目录
    xdebug.profiler_output_name string类型,默认值=cachegrind.out.%p  指定性能分析信息文件的名称
    xdebug.remote_enable    boolean类型,默认值=0 是否开启远程调试
    xdebug.remote_handler   string类型,默认值=dbgp   指定远程调试的处理协议
    xdebug.remote_host  string类型,默认值=localhost  指定远程调试的主机名
    xdebug.remote_log   string类型,默认值=   指定远程调试的日志文件名
    xdebug.remote_mode  string类型,默认值=req    可以设为req或jit,req表示脚本一开始运行就连接远程客户端,jit表示脚本出错时才连接远程客户端。
    xdebug.remote_port  integer类型,默认值=9000  指定远程调试的端口号
    xdebug.trace_options    integer类型,默认值=0 指定对于之后的请求,追踪文件是追加内容还是覆盖之前内容。
    xdebug.trace_output_dir string类型,默认值=/tmp   指定追踪文件的存放目录
    xdebug.trace_output_name    string类型,默认值=trace.%c   指定追踪文件的名称
    

    PhpStorm 配置

    先配置一个 server(一定要配置目录映射),打开
    菜单栏->File->Settings->Languages & Frameworks->PHP->Server
    开启一下 9000 端口 的监听,打开
    菜单栏->Run->Start Listing for PHP Debug Connections

    调试

    chrome 可以安装一个插件xdebug helper,需要调试的页面打开debug,会设置一个cookie来自动加调试标识。
    在代码中打断点,访问浏览器,phpstorm 就会跳出一个调试栏,显示调试信息。

  • laravel 订单通知队列

    需求

    订单完成支付后通知服务器已到账,通知失败则重试,最多3次,第二次5秒后,第三次10秒后

    实现方案一

    生成任务类

    php artisan make:job PaymentNotify
    

    命令将会在app/Jobs目录下生成一个新的类,编辑:

    < ?php
    
    namespace App\Jobs;
    
    use App\Exceptions\SignException;
    use Illuminate\Bus\Queueable;
    use Illuminate\Queue\SerializesModels;
    use Illuminate\Queue\InteractsWithQueue;
    use Illuminate\Contracts\Queue\ShouldQueue;
    
    class PaymentNotify implements ShouldQueue
    {
        use InteractsWithQueue, Queueable, SerializesModels;
    
        protected $data;
    
        /**
         * Create a new job instance.
         *
         * @param $data
         */
    
        public function __construct($data)
        {
            $this->data = $data;
        }
    
        /**
         * Execute the job.
         *
         * @throws \Exception
         */
        public function handle()
        {
            if ($this->attempts() == 1) {
                //处理订单通知
                info('notify'.$this->attempts());
                if(true){//测试强制通知失败
                    $this->release(5);//手动释放任务回队列,带延时执行时间
                }
            }
    
            if ($this->attempts() == 2) {
                //处理订单通知
                info('notify'.$this->attempts());
                if(true){
                    $this->release(10);
                }
            }
    
            if ($this->attempts() == 3) {
                //处理订单通知
                info('notify'.$this->attempts());
                if(true){
                    throw new SignException('fails');//抛出异常,任务失败,自动入库
                }
    
            }
        }
    
    }
    

    添加测试路由并访问:

    Route::get('/pay/notify', function(){
        dispatch((new \App\Jobs\PaymentNotify(['order'=>time().mt_rand(1000,9999)]))->onQueue('PaymentNotify'));
        return 'ok';
    });
    

    监听队列

    php artisan queue:work  --queue=PaymentNotify --tries=3  --sleep=0
    

    可查看使用说明

    $php artisan queue:work --help
    Usage:
      queue:work [options] [--] [<connection>]
    
    Arguments:
      connection               The name of connection
    
    Options:
          --queue[=QUEUE]      The queue to listen on
          --daemon             Run the worker in daemon mode (Deprecated)
          --once               Only process the next job on the queue
          --delay[=DELAY]      Amount of time to delay failed jobs [default: "0"]
          --force              Force the worker to run even in maintenance mode
          --memory[=MEMORY]    The memory limit in megabytes [default: "128"]
          --sleep[=SLEEP]      Number of seconds to sleep when no job is available [default: "3"]
          --timeout[=TIMEOUT]  The number of seconds a child process can run [default: "60"]
          --tries[=TRIES]      Number of times to attempt a job before logging it failed [default: "0"]
      -h, --help               Display this help message
      -q, --quiet              Do not output any message
      -V, --version            Display this application version
          --ansi               Force ANSI output
          --no-ansi            Disable ANSI output
      -n, --no-interaction     Do not ask any interactive question
          --env[=ENV]          The environment the command should run under
      -v|vv|vvv, --verbose     Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
    

    命令意思是:使用默认队列连接的PaymentNotify队列,失败最多尝试执行3次,轮询新任务之前的等待时间为0(即有任务随时检测执行,不会在无任务时默认等待3秒)

    任务执行情况

    [2017-03-07 16:22:44] local.INFO: notify1  
    [2017-03-07 16:22:49] local.INFO: notify2  
    [2017-03-07 16:22:59] local.INFO: notify3
    

    符合预期

    方案二

    另一种思路是订单完成后直接生成3个队列,

    $order = time().mt_rand(1000,9999);
    
    dispatch((new \App\Jobs\PaymentNotify(['order'=>$order]))->onQueue('PaymentNotify'));
    
    dispatch((new \App\Jobs\PaymentNotify(['order'=>$order]))->onQueue('PaymentNotify')->delay(5));
    
    dispatch((new \App\Jobs\PaymentNotify(['order'=>$order]))->onQueue('PaymentNotify')->delay(10));
    

    后两个队列带延时执行参数,设置最多执行次数为1,执行队列时检测已经通知成功则跳过执行

    总结

    方案一:在处理队列时需判断重试次数,分类处理,业务需求变化需改动多;
    方案二:在redis中相同队列存在多条,业务需求变化处理逻辑相对改动少;
    各有利弊。

  • laravel自定义用户认证

    添加自定义的 Guard

    需要通过Auth门面的extend方法定义自己的认证guard,在App\Providers\AuthServiceProvider的boot方法中实现:

    public function boot()
    {
        $this->registerPolicies();
    
        Auth::extend('XXX', function($app, $name, array $config) {
            // 返回 Illuminate\Contracts\Auth\Guard 实例
            $guard = new XXXGuard($name,Auth::createUserProvider($config['provider']),$this->app['session.store']);
            //事件
            if (method_exists($guard, 'setDispatcher')) {
                $guard->setDispatcher($this->app['events']);
            }
            //请求
            if (method_exists($guard, 'setRequest')) {
                $guard->setRequest($this->app->refresh('request', $guard, 'setRequest'));
            }
            return $guard;
        });
    }
    

    (更多…)