封装一个 koa 分布式锁中间件来解决幂等或重复请求的问题
在后端并不是写完一个接口的业务逻辑就能投入使用的,接口的优化更是一个难点与麻烦之处(下面的内容我们不考虑前端的处理,因为不能完全靠前端,前后端都需要做自己的处理工作)1.幂等性:所谓幂等性是指一个接口不论发送多少个相同请求,最后都会产生相同的结果
例如: 根据 Restful API 接口规范:把 CRUD 分为 get(查询),post(新增),delete(删除),put(修改)
GET:查询条件下,不论用户对数据库查询多少次,都不会对数据库的数据造成,所以这天生就是一个幂等接口 POST:新增条件下,如果用户多次发送相同的增加请求,那么数据库将会添加多条相同的记录,所以是一个非幂等接口 PUT:分为两种情况 1.绝对修改: 如果是修改绝对值,例如修改一条 name 为张三的记录,我多次修改最后造成的结果都是一样的(只有一条张三的结果被删除),所以这是一个幂等接口 2.相对修改: 如果是修改相对值,例如修改一张表中 score 最高的记录(select top 1 score from xxx),我多次修改最后造成的结果是不一样的,你发送几次接口,我就会删除几次最高的,所以这是一个非幂等接口
DELETE:也分为两种情况(与 PUT 相同,就不介绍了,也是相对与绝对的问题)所以为了安全性,后端会采用许多方式解决幂等问题,将非幂等的接口转化为幂等接口
2.并发:用户发送请求的时间并不是有规律的,有可能是按顺序一个接一个有序地执行,也有可能在很短时间内发送多个请求抢占同一资源,由于处理请求是异步的,所以不能保证每个都按顺序有序输出,并发也可以细分成两种
1.多个用户抢占同一资源: 例如:100 个人短时间内预约同一个医生,但是医生只能被预约一次,这个时候就会产生高并发,我们必须采取措施保证只有第一个发起请求的能预约到这个医生,后面 99 个都返回预约失败(不是返回请求出错),这时候可以采用阻塞性(多个请求按照顺序排队等待处理)的互斥锁(相同时间内只有一个请求能够获取到锁,其他的请求排队等处理完解锁后再获取),保证这 100 个请求按顺序转为同步(虽然效率会降低,但是保证了正确性)
1.单个用户抢占自己的同一资源: 这里单个用户的并发一般体现在重复请求,但不是完全的参数相同,比如用户短时间内发起两个参数不同的请求修改自己的个人资料(举个例子,实际情况还是很少的,因为前端会采取遮罩层等措施防止用户的这这种行为),但是请求处理是异步的,可能突然受到网络原因,虽然发送顺序是先 1 后 2,但是返回的顺序是先 2 后 1,这样正确性就有问题了,此时可以设置非阻塞性(只有第一个请求上锁然后进行处理,后面的请求全部报错,同一返回服务器繁忙,且不排队等待处理,直接失败)的互斥锁提醒用户已经有请求在处理,不要发送多个请求
3.高并发:高并发是并发的是一种程度的体现,极短的时间内产生了海量的并发请求就是高并发,比如双十一抢购,所以就有了分布式架构(分布式系统,就是一个业务拆分成多个子业务,分布在不同的服务器节点,共同构成的系统称为分布式系统)的出现,一个服务器处理海量的并发压力会巨大甚至宕机,所以分布在不同的服务器节点减轻单一服务器的压力
4.进程锁<线程锁<分布式锁:进程锁:当某个方法或者代码块使用锁时,在同一时刻之多仅有一个线程在执行该段代码(nodejs 的同步代码(异步代码除外)是单进程的,所以无需进程锁)
线程锁:为了控制同一操作系统中多个进程访问一个共享资源,只是因为程序的独立性,各个进程是无法控制其他进程对资源的访问的,但是可以使用本地系统的信号量控制
分布式锁:当多个进程不在同一个系统之中时,使用分布式锁控制多个进程对资源的访问(可以理解为线程锁就就是只有单例的分布式锁)
三锁的范围:进程锁<线程锁<分布式锁三锁作用都是一样的,只是作用的范围大小不同
实战环节:了解了那么多理论知识,下面我来实践一个 nodejs 中分布式锁的中间件封装解决接口幂等问题为什么用分布式锁:1.nodejs 已经有现成的 redlock(以 redlock 分布式锁算法名字命名)包来解决分布式锁的问题,就不用自己再写 redlock 的算法,只需要二次封装为一个中间件,具体 redis 分布式锁的实现可以去看其他人的文章 2.分布式锁范围最大,既可以用于单例也可以用于分布式,这里我是单例实现,自己的小项目也用不着分布式系统
1.在 npm 官网找到 ioredis 和 redlock 两个包 redlock:nodejs 中 redlock 的实现 ioredis:集群式 redis 的实现(上面的 redlock 必须要 ioredis 才行,不能用单例的 redis 包,但是可以在 ioredis 配置单例 redis,总之 ioredis 就是一个功能更加强大的 redis 包)
2.配置 ioredis 和 redlockioredis:思路:创建一个 class 类,把所有 redis 的操作和初始化封装到 Redis 这个类中,最后实例化导出供其他地方使用
注意事项:
1.redis 必须要先安装到你的电脑并配置完并且开启服务才能使用,具体 redis 安装,配置,开启服务实现自行百度 2.如果你要设置 redis 密码,必须先把 redis 配置完密码才能用(自行百度 redis 如何配置密码),不然直接在 nodejs 使用连接会报 auth 错 3.redis 6.0.0 以下不支持用户名,只需要设置密码即可,如果你真的要用户名自行百度配置,但是我觉得一个机子一个 redis 就够了,用户名有点多此一举了
redlock:
注意事项:1.new Redlock 实例的时候第一个参数传入一个数组,里面每一项是 ioredis 的实例,如果像我一样不需要分布式,传入一个实例即可,后面是传入的配置具体查看其文档,此处 retryCount 表示获取锁失败的时候重试的次数,根据官方的解释,这里的 retryCount 设置为 0 够用了,如下图官方解释
3.封装一个分布式锁中间件
4.使用环节(测试验收)
设置了一个测试路由:在路由处理前添加我们设计的中间件 idempotent,不传入参数 isByUser 默认为 false,即全部参数相同就拦截,路由处理没什么,就是等待两秒之后成功输出一句话
第一次:
第二次:
可以看到两次没有任何影响,都是延迟了 2s 后成功返回
多个线程分别发送一次相同请求(并发)这里用多个 api 接口管理工具短时间内轮流发送(处理一个请求需要 2s,所以只要在 2s 之内发送另一个即可)来模拟并发
第一个请求:
第二个请求:
两张图你们很难看出真实情况,但是我我能看到,第一次请求两秒后返回了成功,第二次请求很短时间内直接返回错误(获取不到锁了,代表有重复请求在进行)
这里只给你们演示了一下无参数,无 token 的情况已经成功了,我之后也测试了 isByUser 和有无 token 的有效性,只是没有放出来,但也是没有问题的,isByUser 是我认为比较常用的两种情况:全部参数和用户 id+接口地址的判断方式,如果有其它想法,也可以自定义传入自己想锁定的 key 由什么参数决定,这里你们就二次封装即可,我个人感觉 isByUser 已经够用了
5.一个简单的 koa 分布式锁中间件就封装好了
注意事项:redlock 算法并非绝对安全,如果过期时间设置的太短(小于接口处理时间)会出现接口还没处理完就自动释放锁了,然后出现其他线程也可以获取到锁,就失去了安全性(Java 中的 redisson 里有个 watchdog 自动续期可以解决这个问题,但是这里是 nodejs,目前没有发现封装好 watchdog 机制的分布式锁包,有能力的也可以自己封装,我是能力不够,还是把过期时间设置的稍微长一点好了,但太长也会有其他弊端)
这里的 redlock 是非阻塞性的,上文已经提到,如果获取不到锁会自动报错,请求直接失效而不是排队等候解锁再执行,如果需要阻塞性,可以自己封装,但是我推荐一个其他的包:async-lock 这是一个阻塞性的处理方式,可以形成异步队列按顺序执行而不是非阻塞性地直接抛出错误
评论