lua限流

This commit is contained in:
qiurunze123 2018-12-20 22:53:03 +08:00
parent ed68ca4b99
commit 1e79885aaf
8 changed files with 121 additions and 41 deletions

View File

@ -64,7 +64,7 @@
| 022 |mysql主从复制思路及实操未更新代码 |[解决思路](/docs/mysql-master-slave.md) | | 022 |mysql主从复制思路及实操未更新代码 |[解决思路](/docs/mysql-master-slave.md) |
| 023 |如何进行分库分表 |[解决思路](/docs/mysql-master-slave.md) | | 023 |如何进行分库分表 |[解决思路](/docs/mysql-master-slave.md) |
| 024 |秒杀类似场景sql的写法注意事项有哪些|[解决思路](/docs/mysql-master-slave.md) | | 024 |秒杀类似场景sql的写法注意事项有哪些|[解决思路](/docs/mysql-master-slave.md) |
| 025 |如何利用lua脚本进行原子性 -- 操作限流?|[解决思路](/docs/mysql-master-slave.md) | | 025 |如何利用lua脚本进行操作限流与分布式锁(可保证原子性)|[解决思路](/docs/redis-good.md) |
#### [分布式系统发展历程(已更新)](/docs/fenbushi.md) #### [分布式系统发展历程(已更新)](/docs/fenbushi.md)
#### [分布式系统](/docs/redis-code.md) #### [分布式系统](/docs/redis-code.md)

View File

@ -4,11 +4,11 @@
>目标 希望大家仔细研究redis.conf配置文件-本文很多基础的一带而过) >目标 希望大家仔细研究redis.conf配置文件-本文很多基础的一带而过)
2.redis分布式锁zk分布式锁,lua脚本使用 2.redis分布式锁,zk分布式锁,lua脚本限流,lua分布式锁
3.redis持久化策略 3.redis持久化策略
4.redis集群 4.redis集群
5.redis的简单操练 5.redis的简单操练
整理大部分常用的场景与使用如果有疑问或者你不懂的地方请联系我! 整理大部分常用的场景与使用,如果有疑问或者你不懂的地方请联系我!
#### 1 redis分布式锁 #### 1 redis分布式锁
@ -18,15 +18,15 @@
**redis分布式锁解决什么问题** **redis分布式锁解决什么问题**
1.一个进程中的多个线程,多个线程并发访问同一个资源的时候,如何解决线程安全问题。 1.一个进程中的多个线程,多个线程并发访问同一个资源的时候,如何解决线程安全问题。
2.一个分布式架构系统中的两个模块同时去访问一个文件对文件进行读写操作 2.一个分布式架构系统中的两个模块同时去访问一个文件对文件进行读写操作
3.多个应用对同一条数据做修改的时候如何保证数据的安全性 3.多个应用对同一条数据做修改的时候,如何保证数据的安全性
在但一个进程中我们可以用到synchronized、lock之类的同步操作去解决但是对于分布式架构下多进程的情况下 在但一个进程中,我们可以用到synchronized、lock之类的同步操作去解决,但是对于分布式架构下多进程的情况下,
如何做到跨进程的锁。就需要借助一些第三方手段来完成 如何做到跨进程的锁。就需要借助一些第三方手段来完成
**设计一个分布式所需要解决的问题分布式锁解决方案(数据库方案)** **设计一个分布式所需要解决的问题,分布式锁解决方案(数据库方案)**
数据库解决方式创建一个表叫做LOCK表 数据库解决方式,创建一个表叫做LOCK表
lock( lock(
id int(11) id int(11)
methodName varchar(100),--锁定的方法名称 methodName varchar(100),--锁定的方法名称
@ -35,7 +35,7 @@
unique key mn (method) --唯一约束 unique key mn (method) --唯一约束
) )
在执行方法的时候获取锁的伪代码 或者for update行锁 或者 乐观锁方式也是可以的 在执行方法的时候,获取锁的伪代码 或者for update行锁 或者 乐观锁方式也是可以的
try{ try{
exec insert into lock(methodName,memo) values(method,desc); exec insert into lock(methodName,memo) values(method,desc);
return true; return true;
@ -48,22 +48,22 @@
**(数据库方案)存在的问题以及思考** **(数据库方案)存在的问题以及思考**
1.锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁 1.锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁
2.锁是非阻塞的数据的insert操作一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列 2.锁是非阻塞的,数据的insert操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列
要想再次获得锁就要再次触发获得锁操作 要想再次获得锁就要再次触发获得锁操作
3.锁是非重入的同一个线程在没有释放锁之前无法再次获得该锁 3.锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁
**ZK方案** **ZK方案**
ZK方案实现之前你要先了解ZK关于他的节点的几个特性 ZK方案实现之前你要先了解ZK关于他的节点的几个特性
有序节点:假如当前有一个父节点为/lock我们可以在这个父节点下面创建子节点zookeeper提供了一个可选的有序特性 有序节点:假如当前有一个父节点为/lock,我们可以在这个父节点下面创建子节点zookeeper提供了一个可选的有序特性
例如我们可以创建子节点“/lock/node-”并且指明有序那么zookeeper在生成子节点时会根据当前的子节点数量自动添加整数序号 例如我们可以创建子节点“/lock/node-”并且指明有序,那么zookeeper在生成子节点时会根据当前的子节点数量自动添加整数序号
也就是说如果是第一个创建的子节点,那么生成的子节点为/lock/node-0000000000下一个节点则为/lock/node-0000000001依次类推 也就是说如果是第一个创建的子节点,那么生成的子节点为/lock/node-0000000000,下一个节点则为/lock/node-0000000001,依次类推
临时节点:客户端可以建立一个临时节点,在会话结束或者会话超时后,zookeeper会自动删除该节点 临时节点:客户端可以建立一个临时节点,在会话结束或者会话超时后,zookeeper会自动删除该节点
事件监听:在读取数据时,我们可以同时对节点设置事件监听,当节点数据或结构变化时,zookeeper会通知客户端 事件监听:在读取数据时,我们可以同时对节点设置事件监听,当节点数据或结构变化时,zookeeper会通知客户端
当前zookeeper有如下四种事件 当前zookeeper有如下四种事件
1节点创建 1节点创建
2节点删除 2节点删除
@ -72,15 +72,15 @@
获取分布式锁的流程 ------- 假设所空间的根节点为/lock 获取分布式锁的流程 ------- 假设所空间的根节点为/lock
1.客户端连接zookeeper并在/lock下创建临时的且有序的子节点 1.客户端连接zookeeper,并在/lock下创建临时的且有序的子节点
第一个客户端对应的子节点为/lock/lock-0000000000,第二个为/lock/lock-0000000001以此类推 第一个客户端对应的子节点为/lock/lock-0000000000,第二个为/lock/lock-0000000001,以此类推
2.--避免羊群效应--客户端获取/lock下的子节点列表,判断自己创建的子节点是否为当前子节点列表中序号最小的子节点, 2.--避免羊群效应--客户端获取/lock下的子节点列表,判断自己创建的子节点是否为当前子节点列表中序号最小的子节点,
如果是则认为获得锁,否则监听刚好在自己之前一位的子节点删除消息,获得子节点变更通知后重复此步骤直至获得锁 如果是则认为获得锁,否则监听刚好在自己之前一位的子节点删除消息,获得子节点变更通知后重复此步骤直至获得锁
3.实行业务代码 3.实行业务代码
4.流程完成后删除对应的子节点并释放锁 Watch机制 4.流程完成后,删除对应的子节点并释放锁 Watch机制
对应分布式开源包Curator 对应分布式开源包Curator
![整体流程](https://raw.githubusercontent.com/qiurunze123/imageall/master/zk.png) ![整体流程](https://raw.githubusercontent.com/qiurunze123/imageall/master/zk.png)
@ -89,38 +89,38 @@
Redis 中有许多的命令都可以实现分布式锁,但是比较常用的是SETNX这个命令来实现 Redis 中有许多的命令都可以实现分布式锁,但是比较常用的是SETNX这个命令来实现
有多种方案代码: 有多种方案代码:
1.获取锁释放锁 代码在redismanager 里面 (简单版) 1.获取锁,释放锁 代码在redismanager 里面 (简单版)
![整体流程](https://raw.githubusercontent.com/qiurunze123/imageall/master/redislock1.png) ![整体流程](https://raw.githubusercontent.com/qiurunze123/imageall/master/redislock1.png)
![整体流程](https://raw.githubusercontent.com/qiurunze123/imageall/master/redislock2.png) ![整体流程](https://raw.githubusercontent.com/qiurunze123/imageall/master/redislock2.png)
closeOrder也有 不过是另一种!比较复杂!! closeOrder也有 不过是另一种!比较复杂!!
加入时间对比如果当前时间已经大与释放锁的时间 加入时间对比如果当前时间已经大与释放锁的时间
说明已经可以释放这个锁重新在获取锁setget方法可以把之前的锁去掉在重新获取,旧值在于之前的 说明已经可以释放这个锁重新在获取锁,setget方法可以把之前的锁去掉在重新获取,旧值在于之前的
值比较如果无变化说明这个期间没有人获取或者操作这个redis锁则可以重新获取 值比较,如果无变化说明这个期间没有人获取或者操作这个redis锁,则可以重新获取
![整体流程](https://raw.githubusercontent.com/qiurunze123/imageall/master/redislock3.png) ![整体流程](https://raw.githubusercontent.com/qiurunze123/imageall/master/redislock3.png)
redis有成熟的框架redission redis有成熟的框架redission
**Redis多路复用机制看不看都行** **Redis多路复用机制看不看都行**
linux的内核会把所有外部设备都看作一个文件来操作,对一个文件的读写操作会调用内核提供的系统命令, linux的内核会把所有外部设备都看作一个文件来操作,对一个文件的读写操作会调用内核提供的系统命令,
返回一个 file descriptor文件描述符。对于一个socket的读写也会有响应的描述符称为socketfd(socket 描述符)。 返回一个 file descriptor文件描述符。对于一个socket的读写也会有响应的描述符,称为socketfd(socket 描述符)。
而IO多路复用是指内核一旦发现进程指定的一个或者多个文件描述符IO条件准备好以后就通知该进程 而IO多路复用是指内核一旦发现进程指定的一个或者多个文件描述符IO条件准备好以后就通知该进程
IO多路复用又称为事件驱动操作系统提供了一个功能当某个socket可读或者可写的时候它会给一个通知。 IO多路复用又称为事件驱动,操作系统提供了一个功能,当某个socket可读或者可写的时候,它会给一个通知。
当配合非阻塞socket使用时只有当系统通知我哪个描述符可读了我才去执行read操作可以保证每次read都能读到有效数据。 当配合非阻塞socket使用时,只有当系统通知我哪个描述符可读了,我才去执行read操作,可以保证每次read都能读到有效数据。
操作系统的功能通过select/pool/epoll/kqueue之类的系统调用函数来使用这些函数可以同时监视多个描述符的读写就绪情况 操作系统的功能通过select/pool/epoll/kqueue之类的系统调用函数来使用,这些函数可以同时监视多个描述符的读写就绪情况
这样多个描述符的I/O操作都能在一个线程内并发交替完成这就叫I/O多路复用这里的复用指的是同一个线程 ,这样多个描述符的I/O操作都能在一个线程内并发交替完成,这就叫I/O多路复用,这里的复用指的是同一个线程
多路复用的优势在于用户可以在一个线程内同时处理多个socket的 io请求。达到同一个线程同时处理多个IO请求的目的。 多路复用的优势在于用户可以在一个线程内同时处理多个socket的 io请求。达到同一个线程同时处理多个IO请求的目的。
而在同步阻塞模型中必须通过多线程的方式才能达到目的 而在同步阻塞模型中,必须通过多线程的方式才能达到目的
**Redis2.6以后)--lua脚本** **Redis2.6以后)--lua脚本**
1. 减少网络开销,在Lua脚本中可以把多个命令放在同一个脚本中运行 1.减少网络开销,在Lua脚本中可以把多个命令放在同一个脚本中运行
2. 原子操作redis会将整个脚本作为一个整体执行中间不会被其他命令插入。换句话说编写脚本的过程中无需担心会出现竞态条件 2.原子操作,redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。换句话说,编写脚本的过程中无需担心会出现竞态条件
3. 复用性客户端发送的脚本会永远存储在redis中这意味着其他客户端可以复用这一脚本来完成同样的逻辑 3.复用性,客户端发送的脚本会永远存储在redis中,这意味着其他客户端可以复用这一脚本来完成同样的逻辑
4.脚本可以通过return 来返回客户端
例子: 利用lua脚本进行电话号或则IP限流 例子: 利用lua脚本进行电话号或则IP限流 实例 具体请看 redislua类
KEYS[1] ARGV[1] ARGV[2] key 参数1 参数2 KEYS[1] ARGV[1] ARGV[2] key 参数1 参数2
local num=redis.call('incr',KEYS[1]) local num=redis.call('incr',KEYS[1])
if tonumber(num)==1 then if tonumber(num)==1 then
@ -131,6 +131,28 @@
else else
return 1 return 1
end end
**Redis2.6以后)--lua--EVALSHA命令**
考虑到我们通过eval执行lua脚本,脚本比较长的情况下,每次调用脚本都需要把整个脚本传给redis
比较占用带宽。为了解决这个问题,redis提供了EVALSHA命令允许开发者通过脚本内容的SHA1摘要来执行脚本。该命令的用法和EVAL一样,
只不过是将脚本内容替换成脚本内容的SHA1摘要
1. Redis在执行EVAL命令时会计算脚本的SHA1摘要并记录在脚本缓存中
2. 执行EVALSHA命令时Redis会根据提供的摘要从脚本缓存中查找对应的脚本内容
如果找到了就执行脚本,否则返回“NOSCRIPT No matching script,Please use EVAL”
**Redis2.6以后)--lua脚本运行限制**
redis的脚本执行是原子的,即脚本执行期间Redis不会执行其他命令
所有的命令必须等待脚本执行完以后才能执行。为了防止某个脚本执行时间过程导致Redis无法提供服务
Redis提供了lua-time-limit参数限制脚本的最长运行时间--默认是5秒钟
当脚本运行时间超过这个限制后,Redis将开始接受其他命令但不会执行以确保脚本的原子性,而是返回BUSY的错误
在第一个窗口中执行lua脚本的死循环
eval “while true do end” 0 在第二个窗口中运行get hello
最后第二个窗口的运行结果是Busy, 可以通过script kill命令终止正在执行的脚本
如果当前执行的lua脚本对redis的数据进行了修改,比如set操作,那么script kill命令没办法终止脚本的运行,
因为要保证lua脚本的原子性。如果执行一部分终止了,就违背了这一个原则
在这种情况下,只能通过 shutdown nosave命令强行终止

View File

@ -1,6 +1,7 @@
package com.geekq.miaosha.controller; package com.geekq.miaosha.controller;
import com.geekq.miaosha.common.resultbean.ResultGeekQ; import com.geekq.miaosha.common.resultbean.ResultGeekQ;
import com.geekq.miaosha.redis.redismanager.RedisLua;
import com.geekq.miaosha.service.MiaoShaUserService; import com.geekq.miaosha.service.MiaoShaUserService;
import com.geekq.miaosha.vo.LoginVo; import com.geekq.miaosha.vo.LoginVo;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -24,6 +25,7 @@ public class LoginController {
@RequestMapping("/to_login") @RequestMapping("/to_login")
public String tologin(LoginVo loginVo) { public String tologin(LoginVo loginVo) {
RedisLua.getLUa();
logger.info(loginVo.toString()); logger.info(loginVo.toString());
return "login"; return "login";
} }

View File

@ -0,0 +1,18 @@
package com.geekq.miaosha.controller;
import com.geekq.miaosha.redis.redismanager.RedisLua;
import com.geekq.miaosha.vo.LoginVo;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/lua")
public class TestLuaController {
@RequestMapping("/testlua")
public void lua() {
RedisLua.getLUa();
}
}

View File

@ -0,0 +1,36 @@
package com.geekq.miaosha.redis.redismanager;
import redis.clients.jedis.Jedis;
import java.util.ArrayList;
import java.util.List;
public class RedisLua {
/**
* 未完成 evalsha更方便
*/
public static void getLUa() {
Jedis jedis = null;
try {
jedis = RedisManager.getJedis();
} catch (Exception e) {
e.printStackTrace();
}
String lua =
"local num=redis.call('incr',KEYS[1]) if tonumber(num)==1 " +
"then redis.call('expire',KEYS[1],ARGV[1]) " +
"return 1 elseif tonumber(num)>" +
"tonumber(ARGV[2]) then return 0 else return 1 end";
List<String> keys = new ArrayList<String>();
keys.add("ip:limit:127.0.0.1");
List<String> argves = new ArrayList<String>();
argves.add("6000");
argves.add("5");
jedis.auth("youxin11");
Object object = jedis.eval(lua, keys, argves);
System.out.println(object);
}
}

View File

@ -21,4 +21,6 @@ public class RedisManager {
} }
throw new Exception("Jedispool was not init !!!"); throw new Exception("Jedispool was not init !!!");
} }
} }

View File

@ -46,7 +46,7 @@ public class OrderCloseTask {
// log.info("关闭订单定时任务结束"); // log.info("关闭订单定时任务结束");
// } // }
@Scheduled(cron = "0/1 * * * * ?") // @Scheduled(cron = "0/1 * * * * ?")
public void closeOrderTaskV3(){ public void closeOrderTaskV3(){
log.info("关闭订单定时任务启动"); log.info("关闭订单定时任务启动");
long lockTime = 5000; long lockTime = 5000;

View File

@ -20,7 +20,7 @@ mybatis.config-locations=classpath:mybatis/config.xml
#datasource #datasource
spring.datasource.url=jdbc:mysql://39.107.245.253:3306/miaosha?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false spring.datasource.url=jdbc:mysql://39.107.245.253:3306/miaosha?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false
spring.datasource.username=root spring.datasource.username=root
spring.datasource.password=xxxx spring.datasource.password=nihaoma
spring.datasource.driver-class-name=com.mysql.jdbc.Driver spring.datasource.driver-class-name=com.mysql.jdbc.Driver
#druid #druid
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
@ -49,7 +49,7 @@ spring.resources.static-locations=classpath:/static/
redis.host=39.107.245.253 redis.host=39.107.245.253
redis.port=6379 redis.port=6379
redis.timeout=100 redis.timeout=100
redis.password=xxxx redis.password=youxin11
redis.poolMaxTotal=1000 redis.poolMaxTotal=1000
redis.poolMaxIdle=500 redis.poolMaxIdle=500
redis.poolMaxWait=500 redis.poolMaxWait=500