首页
留言
导航
统计
Search
1
追番推荐!免费看动漫的网站 - 支持在线观看和磁力下载
2,522 阅读
2
推荐31个docker应用,每一个都很实用
1,314 阅读
3
PVE自动启动 虚拟机 | 容器 顺序设置及参数说明
935 阅读
4
一条命令,永久激活!Office 2024!
618 阅读
5
优选 Cloudflare 官方 / 中转 IP
491 阅读
默认分类
服务器
宝塔
VPS
Docker
OpenWRT
Nginx
群晖
前端编程
Vue
React
Angular
NodeJS
uni-app
后端编程
Java
Python
SpringBoot
SpringCloud
流程引擎
检索引擎
Linux
CentOS
Ubuntu
Debian
数据库
Redis
MySQL
Oracle
虚拟机
VMware
VirtualBox
PVE
Hyper-V
计算机
网络技术
网站源码
主题模板
登录
Search
标签搜索
Java
小程序
Redis
SpringBoot
docker
Typecho
Cloudflare
docker部署
虚拟机
WordPress
群晖
uni-app
CentOS
Vue
Java类库
Linux命令
防火墙配置
Mysql
脚本
Nginx
微醺
累计撰写
264
篇文章
累计收到
11
条评论
首页
栏目
默认分类
服务器
宝塔
VPS
Docker
OpenWRT
Nginx
群晖
前端编程
Vue
React
Angular
NodeJS
uni-app
后端编程
Java
Python
SpringBoot
SpringCloud
流程引擎
检索引擎
Linux
CentOS
Ubuntu
Debian
数据库
Redis
MySQL
Oracle
虚拟机
VMware
VirtualBox
PVE
Hyper-V
计算机
网络技术
网站源码
主题模板
页面
留言
导航
统计
搜索到
264
篇与
的结果
2023-09-25
《Redis学习笔记》
史上最全的 Redis 知识总结!Redis作为稳居世界排名第一的KV内存数据库,同时也是最受欢迎的分布式缓存中间件,是应对高并发,大流量,低延迟业务场景的不二选择。面试必问!今天给大家分享的是某大厂的一位大佬整理的 《Redis学习笔记》 ,图文并茂,特别详细,基本上涵盖了你需要的所有 Redis 所有知识点。建议大家至少看 3 遍。学习资料Redis学习笔记.pdf
2023年09月25日
23 阅读
0 评论
0 点赞
2023-09-21
自建远程桌面连接服务,RustDesk搭建教程
什么时候需要远程协助呢? 可能是远程自己在家的电脑,方便游戏排队或者下载文件;也可能是远程单位电脑,进行远程办公;当然,我觉得更多情况是远程小伙伴,帮小伙伴解决一些技术问题。远程协助,除了可以用向日葵、QQ远程和Todesk等软件,有没有一款安全、经济和快速的软件呢?RustDesk本次教程教给大家如何通过 RustDesk ,部署自己的自建远程平台。主要的优点:安全性:RustDesk使用安全的加密通信协议来保护数据传输,确保远程桌面连接的安全性和隐私保护。跨平台支持:RustDesk支持跨多个操作系统平台,包括Windows、macOS和Linux、Android等,这使得它非常灵活和适用于不同的工作环境。而且从名字可以看出,RustDesk是使用Rust所编写,而使用Rust编写的程序,两大特点就是高效和并发,实际使用上,一台亚马逊入门(1C1G)EC2或者Lightsail(1C2G)给个人或者小型团队使用是没有问题的。准备工具服务器目前, RustDesk 支持 Linux Server 和 Windows Server 服务器;看自己的取舍,Linux比较节约资源,而Windows方便小白上手。为了重新体现我们是Linux技术博主,本次搭建使用 Linux Server 进行演示。根据 RustDesk 官网描述和实际测试,一台1C 1G的亚马逊EC2,中继模式下,8对设备远程依旧不卡!!!如果你是Windows Server用户,那么建议至少选择2C 4G的服务器配置,毕竟需要让Windows Server可以跑起来;本文主要演示Linux Server的方法。RustDesk部署下载服务端Github地址 我们使用的是X86版本服务器,所以这里我们下载X86版本的服务包:拷贝下载地址,到服务器上使用wget进行下载和解压:# 使用wget进行下载1.1.8-2版本(最新版本可以看上述发布地址) wget https://github.com/rustdesk/rustdesk-server/releases/download/1.1.8-2/rustdesk-server-linux-amd64.zip # 使用unzip解压 unzip rustdesk-server-linux-amd64.zip # 重命名解压后文件(方便管理) mv amd64 RustDesk现在,我们的RustDesk前期的准备就完成了。接下来,我们尝试运行,之后再注册为服务。尝试运行我们可以先尝试运行一下;确保可以使用后,我们再使用pm2挂起访问,或者systemctl注册为服务。为了方便我们测试,这里使用screen双开两个虚拟终端,并以前台模式挂起后端进程:# 安装screen sudo apt install screen之后,在刚刚我们的RustDesk解压后的目录内,可以看到相关的文件:RustDesk├── hbbr├── hbbs└── rustdesk-utils解释一下各个作用:hbbs: RustDesk的ID服务,用于分配和注册ID;hbbr: RustDesk的中继服务,主要远程访问就是这个,如果直连远程不行,会使用hbbr进行流量中继。之后,我们使用两个screen进行启动:# 创建一个叫myHbbs的虚拟终端: screen -R myHbbs # 运行hbbs ./hbbs通常情况下就会启动成功:使用screen启动hbbs紧接着,Ctrl+a 和 d 返回主终端,启动 hbbr 。# 创建一个叫yHbb的虚拟终端: screen -R myHbbr # 运行hbbr ./hbbr到此,如果没什么防火墙、安全组问题, RustDesk 就启动完成了。使用screen启动hbbr使用screen启动完成查看这个 RustDesk 目录,就可以发现,多了一些数据库文件和一个证书文件:RustDesk├── db_v2.sqlite3├── db_v2.sqlite3-shm├── db_v2.sqlite3-wal├── hbbr├── hbbs├── id_ed25519├── id_ed25519.pub└── rustdesk-utils我们需要拷贝.pub这个公钥文件,用于接下来的本地配置:拷贝公钥文件比如,我的公钥文件内容:C6bJn7*50nCK3y4=接下来,我们可以进行本地的配置。本地配置到 RustDesk 上的客户端下载地址,下载我们的客户端:RustDesk 客户端 Github 发布地址 通常情况下,我们下载最新的发布版本即可:比如:我这里安装macOS,并打开:点击上面的三个点,进入配置:找到网络配置:解锁网络配置,上方两个输入你的服务器IP,下方的Key输入上一步内我们拷贝的公钥(对于我是C6bJn7*50nCK3y4=),如果你有使用域名,IP部分也可以使用域名(CDN不行):我们没有开启强制加密,所以理论上key不填写,也可以进行连接;只是,不填写key,网络传输可能不安全。到此,我们的配置配置就完成了。我们可以体验一下。注册服务现在,我们回到服务端。刚刚使用的screen可能不太优雅,这里我们准备了两个方案注册为访问,方便管理。使用pm2运行如果要使用pm2运行,需要提前安装nodejs,使用nodejs的软件包管理器(npm或者yarn)安装pm2。安装nodejs的方法,我在之前的文章内已经说的十分详细,这里就不再过多介绍,如果不知道如何操作,可以参考:Windows/macOS/Linux上安装Node.js,并使用NVM管理多版本Node.js 就简单说一下思路和使用命令吧:# 安装nvm git clone https://github.com/nvm-sh/nvm.git ~/.nvm # 追加到环境变量 echo 'export NVM_DIR="$HOME/.nvm"' >> ~/.zshrc echo '[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm' >> ~/.zshrc # 重载环境变量 source ~/.zshrc # 使用nvm安装18版本nodejs nvm install 18 # 使用nodejs的npm安装pm2 npm install -g pm2之后,使用pm2的命令,启动hbbr和hbbs:# 当前正在RustDesk的目录内 pm2 start hbbs pm2 start hbbr之后,就可以发现。RustDesk已经启动。 具体的pm2使用,以后有机会和大家讲解。使用Systemctl运行我其实挺喜欢用 systemctl 的;但是上篇文章,其实已经足够详细,这里就直接给配置的脚本吧,我们需要创建两个Server,首先是 hbbs :[Unit] Description=RustDesk Hbbs After=network.target [Service] User=mintimate Type=simple WorkingDirectory=/home/mintimate/myApplication/RustDesk ExecStart=/home/mintimate/myApplication/RustDesk/hbbs ExecStop=/bin/kill -TERM $MAINPID [Install] WantedBy=multi-user.target之后是 hbbr :[Unit] Description=RustDesk Hbbr After=network.target [Service] User=mintimate Type=simple WorkingDirectory=/home/mintimate/myApplication/RustDesk ExecStart=/home/mintimate/myApplication/RustDesk/hbbr ExecStop=/bin/kill -TERM $MAINPID [Install] WantedBy=multi-user.target到此,使用 Systemctl 注册为服务就完成啦。其实也可以使用 screen ,就是重启系统后,再次启动比较麻烦。Q&A个人的一些使用经验,还有一些常见的问题。如何开启强制加密如果你搭建好了 RustDesk 后,会发发现:不需要设置 key ,也可以进行 RustDesk 的连接。这是因为 RustDesk 默认服务器端会开启加密,但是并不强制。如何你想开启强制加密,可以在启动 hbbr 和 hbbs 的时候,加上参数 -k _ :./hbbr -k _ ./hbbs -k _这样,客户端建立的连接,就需要强制设置好 key ,也就是我们的 *.pub 文件,否则无法建立通讯和远程连接。
2023年09月21日
209 阅读
0 评论
0 点赞
2023-09-21
[VPS评测] - VPS主机评测脚本
新买的 VPS 一般都要测试下性能,收集了网络上常见测试 VPS脚本一、秋水逸冰VPS性能测试脚本wget -qO- bench.sh | bash二、BlueSkyXN综合工具箱wget -O box.sh https://raw.githubusercontent.com/BlueSkyXN/SKY-BOX/main/box.sh && chmod +x box.sh && clear && ./box.sh三、SuperSpeed.sh 修复版bash <(curl -Lso- https://git.io/superspeed_uxh)四、一键检测VPS回程国内三网路由curl https://raw.githubusercontent.com/zhanghanyun/backtrace/main/install.sh -sSf | sh五、流媒体解锁测试一键脚本bash <(curl -L -s https://raw.githubusercontent.com/lmc999/RegionRestrictionCheck/main/check.sh)
2023年09月21日
113 阅读
0 评论
0 点赞
2023-09-21
One Manager 搭建一个美观的网盘
前几天闲着没事,就去逛别人的博客,经常能够看到网盘,感觉很美观,今天就用 One Manager 搭建一个一样美观的网盘1.什么是 One Manager?这是一个利用微软onedrive api将onedrive目录映射成一个云盘的程序(同时支持阿里云盘),类似的程序有很多,比如OneIndex、Pyone、OnePoint等等。2.One Manager 能做什么?通过借助onedrive和阿里云盘等网盘api和refresh_token,把云盘映射到网站上,从而达成直接在网站上上传文件(直接同步到网盘上),用直链直接下载文件,管理文件,并让访客访问的目的。3.如果要搭建,该准备些什么?虚拟主机——近年来,虚拟主机的价格一跌再跌,几乎新手类的入门虚拟主机30左右一年,当然也有免费的羊毛可以薅,这里推荐腾讯云(速度快,有保障),小鱼云(速度快,实惠,不用备案),乐数云(可白嫖),如果找到可行的主机都可以域名——这就不用多说了,想要被访问,域名必不可少,最近的各大厂也是疯狂甩卖白菜价的top,xyz域名(硅云0元,西数1元,腾讯9元,请根据自己需要,以及认为cdn哪个加速快选用),freenom也是一个不错的选择,tk,ml,ga顶级域名都是免费的,但是最近又看了下,貌似失效了,也不知道什么时候恢复One Manager 本体软件GitHub作者链接:https://github.com/qkqpttgf/OneManager-php蓝奏云备用链接:https://wwck.lanzouf.com/iYKMm0l9uzlc(如果打不开请把lanzouf改成lanzoui或者lanzouw)以下是详细教程最最基础部分,先进行虚拟主机域名的解析与绑定首先打开你的虚拟主机的控制面板界面,点击域名绑定(由于各厂商不同,就不放图了)看虚拟主机提示的想要绑定,先解析的链接(如果是ip地址,都行,请看好是用cname还是a记录解析)打开域名管理界面,添加解析记录,如果是ip,大部分情况下用a记录,主机名随便填,@的话,是你访问的云盘就是你的域名,xxx的话,你的云盘访问地址就是xxx.你的域名,在后面选择解析方式,cname或者a,然后把ip或者链接填到记录值一栏,其余的就不用管了在虚拟主机绑定你的域名,记住你要绑定的目录下载源码,打开ftp文件管理进行在线上传,将你下载的源码进行上传,并解压到你绑定的目录,保证那一堆文件在你绑定的目录里面接下来就可以在浏览器输入你的网盘地址,进行设置了由于各个厂商的主机默认配置不同,进入配置时,如果出现伪静态设置错误可自行百度 “One Manager伪静态” 或者复制以下代码复制到目录里面的 .htaccess 文件# # Apache # # LoadModule rewrite_module modules/mod_rewrite.so # # AllowOverride All RewriteEngine On # RewriteCond $1 !^(.well-known) RewriteRule ^(.*) index.php?/$1 [L] ### nginx # rewrite ^/(?!.well-known)(.*)$ /index.php?/$1 last; # ### nginx Subdirectory 在子目录中使用 # location /OneManager2/ { # rewrite ^/(.*)$ /OneManager2/index.php?/$1 last; # } # ### caddy # rewrite { # to index.php?/$1 # } # ### caddy2 Caddyfile # @try_files { # not path /.well-known/* # file { # try_files index.php # } # } # rewrite @try_files {http.matchers.file.relative} # 进入网站设置完成后,可见到这样的界面点击左上角的登录,输入你自己设置的密码登录成功后,登录按钮变成了管理,点击后再点击设置,进行添加盘在添加盘的左侧选择你要添加的盘(推荐阿里云盘和onedrive,在国内速度还行,其余差不多都需要魔法)阿里云盘搭建方法先点击添加盘,按要求输入标签和名称,接下来就要获取refresh tokenhttps://alist.nn.ci/zh/guide/drivers/aliyundrive.html 打开网址,点击获取 token ,用手机阿里云盘扫描二维码,再点击下按钮,就能够获取token再次进入你的添加盘的界面,输入 token ,就可以看到阿里云盘挂载成功了onedrive搭建方法点击添加盘,根据默认的设置直接添加标签和名称,会自动跳转到 microsoft登录,登录就可以挂载成功了如果觉得界面不够美观,可以点击平台变量进行设置https://www.cnblogs.com/braless/p/14173750.html 该网站可以教你如何个性化你的网盘这边建议游客主题设置为 classic ,比较美观结尾注意事项登录成功进行上传文件后,请点击管理-刷新当前目录缓存,游客端方可看见文件
2023年09月21日
44 阅读
0 评论
0 点赞
2023-09-20
JAVA项目自从用了接口请求合并,效率直接加倍!
请求合并到底有什么意义呢?我们来看下图。假设我们3个用户(用户id分别是1、2、3),现在他们都要查询自己的基本信息,请求到服务器,服务器端请求数据库,发出3次请求。我们都知道数据库连接资源是相当宝贵的,那么我们怎么尽可能节省连接资源呢?这里把数据库换成被调用的远程服务,也是同样的道理。我们改变下思路,如下图所示。我们在服务器端把请求合并,只发出一条SQL查询数据库,数据库返回后,服务器端处理返回数据,根据一个唯一请求ID,把数据分组,返回给对应用户。技术手段LinkedBlockQueue 阻塞队列ScheduledThreadPoolExecutor 定时任务线程池CompleteableFuture future 阻塞机制(Java 8 的 CompletableFuture 并没有 timeout 机制,后面优化,使用了队列替代)代码实现查询用户的代码public interface UserService { Map<String, Users> queryUserByIdBatch(List<UserWrapBatchService.Request> userReqs); } @Service public class UserServiceImpl implements UserService { @Resource private UsersMapper usersMapper; @Override public Map<String, Users> queryUserByIdBatch(List<UserWrapBatchService.Request> userReqs) { // 全部参数 List<Long> userIds = userReqs.stream().map(UserWrapBatchService.Request::getUserId).collect(Collectors.toList()); QueryWrapper<Users> queryWrapper = new QueryWrapper<>(); // 用in语句合并成一条SQL,避免多次请求数据库的IO queryWrapper.in("id", userIds); List<Users> users = usersMapper.selectList(queryWrapper); Map<Long, List<Users>> userGroup = users.stream().collect(Collectors.groupingBy(Users::getId)); HashMap<String, Users> result = new HashMap<>(); userReqs.forEach(val -> { List<Users> usersList = userGroup.get(val.getUserId()); if (!CollectionUtils.isEmpty(usersList)) { result.put(val.getRequestId(), usersList.get(0)); } else { // 表示没数据 result.put(val.getRequestId(), null); } }); return result; } }合并请求的实现package com.springboot.sample.service.impl; import com.springboot.sample.bean.Users; import com.springboot.sample.service.UserService; import org.springframework.stereotype.Service; import javax.annotation.PostConstruct; import javax.annotation.Resource; import java.util.*; import java.util.concurrent.*; /*** * zzq * 包装成批量执行的地方 * */ @Service public class UserWrapBatchService { @Resource private UserService userService; /** * 最大任务数 **/ public static int MAX_TASK_NUM = 100; /** * 请求类,code为查询的共同特征,例如查询商品,通过不同id的来区分 * CompletableFuture将处理结果返回 */ public class Request { // 请求id 唯一 String requestId; // 参数 Long userId; //TODO Java 8 的 CompletableFuture 并没有 timeout 机制 CompletableFuture<Users> completableFuture; public String getRequestId() { return requestId; } public void setRequestId(String requestId) { this.requestId = requestId; } public Long getUserId() { return userId; } public void setUserId(Long userId) { this.userId = userId; } public CompletableFuture getCompletableFuture() { return completableFuture; } public void setCompletableFuture(CompletableFuture completableFuture) { this.completableFuture = completableFuture; } } /* LinkedBlockingQueue是一个阻塞的队列,内部采用链表的结果,通过两个ReenTrantLock来保证线程安全 LinkedBlockingQueue与ArrayBlockingQueue的区别 ArrayBlockingQueue默认指定了长度,而LinkedBlockingQueue的默认长度是Integer.MAX_VALUE,也就是无界队列,在移除的速度小于添加的速度时,容易造成OOM。 ArrayBlockingQueue的存储容器是数组,而LinkedBlockingQueue是存储容器是链表 两者的实现队列添加或移除的锁不一样,ArrayBlockingQueue实现的队列中的锁是没有分离的,即添加操作和移除操作采用的同一个ReenterLock锁, 而LinkedBlockingQueue实现的队列中的锁是分离的,其添加采用的是putLock,移除采用的则是takeLock,这样能大大提高队列的吞吐量, 也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。 */ private final Queue<Request> queue = new LinkedBlockingQueue(); @PostConstruct public void init() { //定时任务线程池,创建一个支持定时、周期性或延时任务的限定线程数目(这里传入的是1)的线程池 ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1); scheduledExecutorService.scheduleAtFixedRate(() -> { int size = queue.size(); //如果队列没数据,表示这段时间没有请求,直接返回 if (size == 0) { return; } List<Request> list = new ArrayList<>(); System.out.println("合并了 [" + size + "] 个请求"); //将队列的请求消费到一个集合保存 for (int i = 0; i < size; i++) { // 后面的SQL语句是有长度限制的,所以还要做限制每次批量的数量,超过最大任务数,等下次执行 if (i < MAX_TASK_NUM) { list.add(queue.poll()); } } //拿到我们需要去数据库查询的特征,保存为集合 List<Request> userReqs = new ArrayList<>(); for (Request request : list) { userReqs.add(request); } //将参数传入service处理, 这里是本地服务,也可以把userService 看成RPC之类的远程调用 Map<String, Users> response = userService.queryUserByIdBatch(userReqs); //将处理结果返回各自的请求 for (Request request : list) { Users result = response.get(request.requestId); request.completableFuture.complete(result); //completableFuture.complete方法完成赋值,这一步执行完毕,下面future.get()阻塞的请求可以继续执行了 } }, 100, 10, TimeUnit.MILLISECONDS); //scheduleAtFixedRate是周期性执行 schedule是延迟执行 initialDelay是初始延迟 period是周期间隔 后面是单位 //这里我写的是 初始化后100毫秒后执行,周期性执行10毫秒执行一次 } public Users queryUser(Long userId) { Request request = new Request(); // 这里用UUID做请求id request.requestId = UUID.randomUUID().toString().replace("-", ""); request.userId = userId; CompletableFuture<Users> future = new CompletableFuture<>(); request.completableFuture = future; //将对象传入队列 queue.offer(request); //如果这时候没完成赋值,那么就会阻塞,直到能够拿到值 try { return future.get(); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } return null; } }控制层调用/*** * 请求合并 * */ @RequestMapping("/merge") public Callable<Users> merge(Long userId) { return new Callable<Users>() { @Override public Users call() throws Exception { return userBatchService.queryUser(userId); } }; }Callable 是什么可以参考:https://blog.csdn.net/baidu_19473529/article/details/123596792模拟高并发查询的代码package com.springboot.sample; import org.springframework.web.client.RestTemplate; import java.util.Random; import java.util.concurrent.CountDownLatch; public class TestBatch { private static int threadCount = 30; private final static CountDownLatch COUNT_DOWN_LATCH = new CountDownLatch(threadCount); //为保证30个线程同时并发运行 private static final RestTemplate restTemplate = new RestTemplate(); public static void main(String[] args) { for (int i = 0; i < threadCount; i++) {//循环开30个线程 new Thread(new Runnable() { public void run() { COUNT_DOWN_LATCH.countDown();//每次减一 try { COUNT_DOWN_LATCH.await(); //此处等待状态,为了让30个线程同时进行 } catch (InterruptedException e) { e.printStackTrace(); } for (int j = 1; j <= 3; j++) { int param = new Random().nextInt(4); if (param <=0){ param++; } String responseBody = restTemplate.getForObject("http://localhost:8080/asyncAndMerge/merge?userId=" + param, String.class); System.out.println(Thread.currentThread().getName() + "参数 " + param + " 返回值 " + responseBody); } } }).start(); } } }测试效果要注意的问题Java 8 的 CompletableFuture 并没有 timeout 机制后面的SQL语句是有长度限制的,所以还要做限制每次批量的数量,超过最大任务数,等下次执行(本例中加了MAX_TASK_NUM判断)使用队列的超时解决Java 8 的 CompletableFuture 并没有 timeout 机制核心代码package com.springboot.sample.service.impl; import com.springboot.sample.bean.Users; import com.springboot.sample.service.UserService; import org.springframework.stereotype.Service; import javax.annotation.PostConstruct; import javax.annotation.Resource; import java.util.*; import java.util.concurrent.*; /*** * zzq * 包装成批量执行的地方,使用queue解决超时问题 * */ @Service public class UserWrapBatchQueueService { @Resource private UserService userService; /** * 最大任务数 **/ public static int MAX_TASK_NUM = 100; /** * 请求类,code为查询的共同特征,例如查询商品,通过不同id的来区分 * CompletableFuture将处理结果返回 */ public class Request { // 请求id String requestId; // 参数 Long userId; // 队列,这个有超时机制 LinkedBlockingQueue<Users> usersQueue; public String getRequestId() { return requestId; } public void setRequestId(String requestId) { this.requestId = requestId; } public Long getUserId() { return userId; } public void setUserId(Long userId) { this.userId = userId; } public LinkedBlockingQueue<Users> getUsersQueue() { return usersQueue; } public void setUsersQueue(LinkedBlockingQueue<Users> usersQueue) { this.usersQueue = usersQueue; } } /* LinkedBlockingQueue是一个阻塞的队列,内部采用链表的结果,通过两个ReenTrantLock来保证线程安全 LinkedBlockingQueue与ArrayBlockingQueue的区别 ArrayBlockingQueue默认指定了长度,而LinkedBlockingQueue的默认长度是Integer.MAX_VALUE,也就是无界队列,在移除的速度小于添加的速度时,容易造成OOM。 ArrayBlockingQueue的存储容器是数组,而LinkedBlockingQueue是存储容器是链表 两者的实现队列添加或移除的锁不一样,ArrayBlockingQueue实现的队列中的锁是没有分离的,即添加操作和移除操作采用的同一个ReenterLock锁, 而LinkedBlockingQueue实现的队列中的锁是分离的,其添加采用的是putLock,移除采用的则是takeLock,这样能大大提高队列的吞吐量, 也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。 */ private final Queue<Request> queue = new LinkedBlockingQueue(); @PostConstruct public void init() { //定时任务线程池,创建一个支持定时、周期性或延时任务的限定线程数目(这里传入的是1)的线程池 ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1); scheduledExecutorService.scheduleAtFixedRate(() -> { int size = queue.size(); //如果队列没数据,表示这段时间没有请求,直接返回 if (size == 0) { return; } List<Request> list = new ArrayList<>(); System.out.println("合并了 [" + size + "] 个请求"); //将队列的请求消费到一个集合保存 for (int i = 0; i < size; i++) { // 后面的SQL语句是有长度限制的,所以还要做限制每次批量的数量,超过最大任务数,等下次执行 if (i < MAX_TASK_NUM) { list.add(queue.poll()); } } //拿到我们需要去数据库查询的特征,保存为集合 List<Request> userReqs = new ArrayList<>(); for (Request request : list) { userReqs.add(request); } //将参数传入service处理, 这里是本地服务,也可以把userService 看成RPC之类的远程调用 Map<String, Users> response = userService.queryUserByIdBatchQueue(userReqs); for (Request userReq : userReqs) { // 这里再把结果放到队列里 Users users = response.get(userReq.getRequestId()); userReq.usersQueue.offer(users); } }, 100, 10, TimeUnit.MILLISECONDS); //scheduleAtFixedRate是周期性执行 schedule是延迟执行 initialDelay是初始延迟 period是周期间隔 后面是单位 //这里我写的是 初始化后100毫秒后执行,周期性执行10毫秒执行一次 } public Users queryUser(Long userId) { Request request = new Request(); // 这里用UUID做请求id request.requestId = UUID.randomUUID().toString().replace("-", ""); request.userId = userId; LinkedBlockingQueue<Users> usersQueue = new LinkedBlockingQueue<>(); request.usersQueue = usersQueue; //将对象传入队列 queue.offer(request); //取出元素时,如果队列为空,给定阻塞多少毫秒再队列取值,这里是3秒 try { return usersQueue.poll(3000,TimeUnit.MILLISECONDS); } catch (InterruptedException e) { e.printStackTrace(); } return null; } } ...省略... @Override public Map<String, Users> queryUserByIdBatchQueue(List<UserWrapBatchQueueService.Request> userReqs) { // 全部参数 List<Long> userIds = userReqs.stream().map(UserWrapBatchQueueService.Request::getUserId).collect(Collectors.toList()); QueryWrapper<Users> queryWrapper = new QueryWrapper<>(); // 用in语句合并成一条SQL,避免多次请求数据库的IO queryWrapper.in("id", userIds); List<Users> users = usersMapper.selectList(queryWrapper); Map<Long, List<Users>> userGroup = users.stream().collect(Collectors.groupingBy(Users::getId)); HashMap<String, Users> result = new HashMap<>(); // 数据分组 userReqs.forEach(val -> { List<Users> usersList = userGroup.get(val.getUserId()); if (!CollectionUtils.isEmpty(usersList)) { result.put(val.getRequestId(), usersList.get(0)); } else { // 表示没数据 , 这里要new,不然加入队列会空指针 result.put(val.getRequestId(), new Users()); } }); return result; } ...省略...小结请求合并,批量的办法能大幅节省被调用系统的连接资源,本例是以数据库为例,其他RPC调用也是类似的道理。缺点就是请求的时间在执行实际的逻辑之前增加了等待时间,不适合低并发的场景。
2023年09月20日
12 阅读
0 评论
0 点赞
2023-09-04
消息推送 架构设计(推荐看看)
构建企业级统一基础推送服务,支持通过多渠道推送,能够统一集成的电子邮件、短信、聊天、钉钉、企业微信和其他公共社交应用:聊天 - 微信Wechat/QQ站内推送通知(移动设备和Web浏览器)站外推送通知(移动设备,APP没有开启)短信(如登录密码、营销活动)电子邮件钉钉企业微信企业级统一基础推送服务,是一个通用特性,适用于所有现代分布式应用,无论采用何种编程语言和技术。推送能力的演进第一阶段(模块化):各自为政、各自封装企业内部,早期业务量比较少,各系统基本都是有自己的推送模块,类型也是五花八门:聊天模块短信模块电子邮件模块websocket 模块各自封装模块比较简单,但是实现分散、各系统模块的质量也很难统一保证。第二阶段(框架化):集成框架为了减少重复性设计、开发成本, 设计了统一的推送框架同一套微服务框架,共用一个统一的推送框架为了解决上述分散实现的问题,企业内部统一实现了一个综合各类推送功能的基础库,供业务方统一调用。聊天基础starter短信基础starter电子邮件基础starterwebsocket 基础starter于是,我们把 springboot-starter 的逻辑封装到了服务治理框架内,微服务服务启动时,每一个服务对各种的starter进行运维管理、配置管理。第三阶段(服务化):推送服务集成到框架,每一套服务,都需要重复性的解决3高问题。推送服务,数据量大,需要解决跨库查询问题推送服务,性能要求高,需要解决高并发问题大数据量、并发量高,意味着:硬件资源投入大运维成本高这样的基础服务,需要进行沉淀,剥离,集中成统一的、基础服务,由专门团队负责维护、迭代、运维。降低重复投入、重复建设成本, 真正的降本增效。于是, 推送框架 演进为 推送服务推送服务在业务系统中的位置一个业务应用, 基本上有很多原子服务编排、整合而来,最终构建出一个完整的架构图。接入层,这是外部请求进入内部系统的门户,所有的请求都必须通过 API 网关。应用层,也被称为聚合层,它为相关业务提供聚合接口,并调用中台服务进行组合。原子服务,包括就是原子技术服务,原子业务服务,根据业务需求提供相关的接口。原子服务为整个架构提供可复用的能力。例如,在B站视频网站平台上,评论服务作为一项原子服务,在B站的视频、文章、社区都需要,那么为了提高复用性,评论服务就可以独立为原子服务,不能与特定需求紧密耦合。在这种情况下, 评论服务,需要供一种可以适应不同场景的复用能力。类似的,文件存储、数据存储、推送服务、身份验证服务等功能,都会沉淀为原子服务,业务开发人员,在原子服务基础上,进行编排、配置、组合,可以快速构建业务应用。推送服务功能要求发送通知对通知进行优先级排序根据客户的保存偏好发送通知支持单个/简单的通知消息和批量通知消息各种通知的分析用例通知消息的报告推送非功能性需求(NFR)高性能:qps > 1W高可用性(HA):99.99%低延迟:TP99 在10ms以下高扩展:可扩展/可插拔的设计,以便添加更多适配器和提供商,与所有通知模块的API集成以及与客户端和服务提供商/供应商的外部集成跨平台:支持Android/iOS移动设备和桌面/笔记本电脑的Web浏览器自伸缩:可在本地(VMware Tanzu)和 AWS、GCP 或 Azure 等公共云服务上扩展负载推送系统设计架构这些解决方案设计的考虑因素和组件包括:1. 通知客户端 这些客户端通过 API 调用请求单个和批量消息。它们将向简单和批量通知服务发送通知消息。简单通知客户端:专门用于发送单个通知的客户端,负责向用户发送单一通知。这些客户端通常用于向特定用户发送重要通知,例如密码找回或账户异常提醒。批量通知客户端:专门用于发送批量通知的客户端,负责向用户批量推送通知。这些客户端通常用于需要通知大量用户的场景,例如企业内部通知或营销活动。2. 通知服务作为入口点的这些服务,通过暴露 REST API 与客户端互动。它们负责构建通知消息,通过调用"模板服务"。这些消息将使用"验证服务"进行验证。简单通知服务:该服务将提供 API,主要负责处理简单通知请求,提供与后端服务集成的 API,以便将通知发送给用户。这种服务通常用于处理较少的通知请求,例如针对特定用户或事件的简单通知。批量通知服务:该服务将提供 API,主要负责处理批量通知请求,提供与后端服务集成的 API,以便批量发送通知。这种服务通常用于处理大量的通知请求,例如企业内部的批量通知或营销活动的批量推送。此服务还将管理通知消息。它将发送的消息持久化到数据库并维护活动日志。可以使用这些服务的 API 重新发送同一条消息。它将提供添加/更新/删除和查看旧消息和新消息的 API。它还将提供 Web 仪表板,该仪表板应具有筛选选项,以根据不同的条件(如日期范围、优先级、模块用户、用户组等)筛选消息。3. 模板服务 此服务主要负责所有可用的一次性密码(OTP)、短信、电子邮件、聊天以及其他推送通知消息的模板管理。它还提供了 REST API ,以便创建、更新、删除和管理模板。除此之外,它还将提供一个用户界面(UI)的仪表板页面,使用户能从网络控制台检查和管理各种消息模板。4. 消息分发服务 定时分发服务:该服务将提供API来安排立即或指定时间的通知。可以是以下任何一种:秒分钟每小时每天每周每月每年自定义频率等。还可能有其他自动触发的服务,基于预定时间进行消息触发。消息验证服务:此服务全权负责根据业务规定和预期格式对通知信息进行核实。批量通知需由授权的系统管理员同意。消息优先级服务:该服务负责对通知进行优先级排序,分为高、中、低三个等级。通知信息具有较高的优先级和有时间限制的到期时间,它们将始终以较高优先级发送。"通用出口处理器"会接收消息并根据相同的优先级从高、中和低三个不同的队列中发送和处理。在非工作时间,可以以低优先级发送批量通知。在交易过程中的应用程序通知可以发送到中优先级,如电子邮件等。企业可以根据通知的重要性确定优先级。5. 事件优先级队列(消息队列) 此服务提供事件中心功能,负责接收通知服务的高、中、低三个优先级的信息。它会根据业务的优先级来发送和接收通知。企业可以根据通知的重要性来设定优先级。服务内部包含三个主题,用于根据业务优先级接收和发送通知:低优先级:主要用于在非工作时间发送批量通知。中优先级:适用于在交易过程中发送的应用程序通知,如电子邮件等。高优先级:通知信息具有较高的优先级和有时间限制的到期时间,它们将始终以较高优先级发送。6. 通用出站处理程序该服务通过轮询事件优先级队列来接收事件中心中的通知信息,并根据其优先级进行处理。高优先级的通知会优先处理"高"队列,依次类推。最后,它通过事件中心将通知信息发送到特定的适配器。此外,该服务还从用户选择服务中获取目标用户/应用程序,以便进行通知的分发。在处理过程中,通用出口处理器会根据事件的优先级进行相应的操作,确保重要事件得到优先处理。这样,企业可以根据通知的优先级来确定处理顺序,从而提高通知的处理效率。除此之外, 通用出站处理程序,还能进行消息的进一步按照通道类型进行分发:该服务将消息发送到各种支持的适配器。这些适配器会根据不同的设备(如桌面/移动设备)和通知类型(如短信/OTP/电子邮件/聊天/推送通知)进行转换。7. 通知适配器 这些转换器将从消息队列(rocketmq)接收传入信息并根据其所支持的格式传递给外部合作伙伴。以下是一些转换器,根据需求可以增加更多:QQ 通知适配器服务微信Wechat 聊天通知适配器服务应用内通知适配器服务电子邮件适配器服务短信适配器服务OTP 适配器服务8. 通道供应商这些是外部的 SAAS(云上/本地)服务提供商,利用它们的基础设施和技术实现实际的通知传递。它们可能是像 AWS SNS、MailChimp 等的付费推送通道服务。QQ 供应商集成服务微信Wechat 供应商集成服务应用推送通知供应商集成服务电子邮件供应商集成服务短信供应商集成服务9. 用户选择服务该服务提供选择目标用户和各种应用程序模块的功能。这可能包括将批量消息发送到特定的用户组或不同的应用程序模块。可能是 AD/IAM/eDirectory/用户数据库/用户组,具体取决于客户的偏好。在服务内部,它将使用"用户配置文件服务"API 来消费和检查客户的通知偏好。10. 用户配置文件服务 此服务提供各种功能,包括管理用户配置文件及其偏好设置。还管理内部用户标识,和外部通道标识之间的关联关系钉钉用户标识 和 用户标识 关联关系企业微信 用户标识 和 用户标识 关联关系用户和邮箱的关联关系等等它还将提供取消订阅通知以及通知接收频率等功能。"通知服务"将依赖于此服务,以便根据用户的通知偏好来发送通知。此外,该服务还可以用于统计和分析用户对通知的偏好,以帮助企业优化通知策略。11. 分析服务 该处理器将负责执行所有的分析工作,识别通知使用情况、趋势并生成报告。它将从分析数据库(Cassandra)和通知数据库中提取所有最终的通知信息,用于分析和报告目的。以下是一些用例:每天/每秒的总通知数哪个通知系统使用最频繁消息的平均大小和频率基于优先级过滤消息等等...12. 通知跟踪器此服务将持续监视事件中心队列并跟踪所有发送的通知。它捕获通知的元数据,如传输时间、传送状态、通信渠道、消息类型等。13. 通知数据库:Mysql数据库集群 通知数据库,用于存储库用于存储所有通知信息,包括发送时间、状态等。它包括一个数据库集群,其中领导者用于执行所有写操作,读取操作则在读取副本/跟随者上进行。这个数据库群集将持久化所有通知,供分析和报告使用。它基于“写入更多,读取更少”的理念。它能提供良好的性能和低延迟,适应大量的通知,因为它内部处理大量的写操作,并与其他数据库节点同步,保持高可用性和可靠性的冗余数据/消息。在任何节点崩溃的情况下,消息将始终可用。
2023年09月04日
78 阅读
0 评论
0 点赞
2023-08-30
Cloudflare加速解析服务-优化大陆访问速度
前言Cloudfalre 加速解析是由 心有网络 向中国大陆用户提供的公共优化服务接入服务节点: cf.13d7s.site接入使用方式类似于其它CDN的CNAME接入,可以为中国大陆用户访问Cloudflare网络节点大幅度加速,累计加速节点60+,其中移动网络为最快,联通最慢。开始前的温馨提示(看一下)1、教程使用阿里云域名演示2、教程过程会cloudflare和阿里云来回切换,所以大家不要以为我教程过程中切换页面之前的页面就把之前的页面关闭了3、教程是在别的地方看见的,原文说的比较简单,我这里会很详细。4、域名商要支持分区解析(大概意思就是能选路,国内网络怎么访问,国外网络怎么访问),推荐阿里云Aliyun、腾讯云DNSpod,如果你的域名不是这两家,末尾我会演示一下在其他地方的域名使用他们的域名解析的5、本教程十分详细,不用担心自己是小白就看不懂(基本是一个教程一个图那种)教程登陆 cloudflare(官网有点卡,耐心等一下,有能力可以挂梯子):https://dash.cloudflare.com/ 有账号我们直接登录,没有账号我们直接注册一个登录。(这里如果我们是第一次弄的话邮箱地址一定要写清楚,后面验证域名的时候会给我们发邮件)登录之后我们添加我们的网站。我这里网站是x-u.cc一进来就会让我们选择服务,这里我们有钱的话可以去买,不想花钱的话我们直接往下面翻,我这里选择白嫖服务这里的话是提示我们域名之前的解析记录,看一下没有问题的话就继续了。(这里如果我们没有解析记录话我们添加一下,然后继续)(记住这里一定要选代理,不代理的话他不会给我们套CDN)然后这里有一大推东西(大概意思就是我们把DNS服务器改成他的),我们不用管,往下找到第四步。这里是我们要修改dns服务器的值我们现在终于可以去打开我们的阿里云了,多开一个页面,登录阿里云:阿里云-计算,为了无法计算的价值 (aliyun.com)登录阿里云之后,我们点击控制台,找到域名:这里我们点x-u.cc,不要去点解析然后进入这个页面,点击修改dns服务器然后修改我们的DNS服务器,这里我其实已经修改好了,但是为了照顾小白,我这里还是再弄一下。这里可以填写两个dns服务,我们直接把之前的cloudflare页面的dns服务值复制过来然后我们DNS就修改好了阿里云这边改好了之后我们切回我们的cloudflare点击下一步了。这里我们选稍后完成进入到这里之后我们等一哈,cloudflare会给我们一个成功的邮件(基本十几秒就到了)。邮箱一到,表示我们已经成功了现在我们去使用itdog测试一下:https://itdog.cn/这里大家其实只会有几个ip,也没有我这么绿,这是因为我之前已经弄好了导致的。好的,目前为止我们已经完成3/2的进度了,马上就可以和我一样快了(滑稽)现在我们切回我们之前阿里云的dns页面,继续修改我们的dns服务器添加dns服务器到能填写4个地址然后我们这里前面两个写上我们阿里云的地址:dns1.hichina.comdns2.hichina.com然后3和4我们写上cloudflare的地址这里我们可以直接复制粘贴到3和4的输入框中:这里会发现我第三个框是红色的,然后确定也是灰色的,这里说明我们第三个框有点问题(一般都是前面有多的空格或者后面有多的空格)我们把多余的空格删除就行了接下来我们点击域名解析:这里我们添加解析:注意:国内(我们需要添加三条解析(中国移动、中国电信、中国联通))我们CNAME解析值是:cf.13d7s.site境外我们CNAME解析值是:你的域名+.cdn.cloudflare.net(比如我是@解析,那我的解析值就是x-u.cc,我的域名值就是x-u.cc.cdn.cloudflare.net,如果我是www解析,那我域名值就是www.x-u.cc.cdn.cloudflare.net)好的,我们现在就已经完成了我们所有过程,我们现在打开itdog看看是否有变化。https://itdog.cn/
2023年08月30日
42 阅读
0 评论
0 点赞
2023-08-14
一个通用型PDF文件处理工具,完全开源,个人免费使用!
🔥本项目系统是一款通用型PDF文件处理工具,包含PDF合并、拆分、旋转、水印、加密、转换等20多项常用功能,完全开源,个人免费使用,界面简洁,简单易用。虽然目前网上关于PDF处理的工具有很多,但是都有一些缺点:专业的PDF编辑软件对于高级一点的功能(添加水印、页面编辑等)需要收费或限制功能在线PDF工具类网站需要上传PDF到服务器处理再下载,有泄露隐私风险各大编程语言的PDF处理库虽然可以免费实现一些高级功能,但是需要一定的编程经验,使用没有图形界面程序方便部分小众工具虽然可以满足部分特殊需求,但是功能较为单一由于PDF处理是一个很常见的需求,为了绕开上述这些限制,提高工作效率,诞生了此项目。本项目具有如下优势:完全本地化:无需联网,不必担心隐私泄露功能丰富:支持包括PDF批量合并、拆分、添加水印、加密/解密、提取、OCR识别在内的20余项功能跨平台:支持在Windows、Mac、Linux设备上使用开源免费界面简洁,使用简单体积小巧(~30M),绿色免安装,随用随开插件化:根据需要选择是否安装额外组件,减小安装包体积功能演示上手指南安装二进制安装去Releases版块下载对应平台的安装包安装即可。编译安装安装go环境、node环境和python环境# 确认go安装成功 go version # 确认 "~/go/bin" 位于PATH环境变量中 echo "export PATH=$PATH:$HOME/go/bin" >> $HOME/.bashrc source $HOME/.bashrc echo $PATH | grep go/bin # 确认nodejs安装成功 npm --version编译项目git clone https://github.com/kevin2li/PDF-Guru.git cd PDF-Guru ROOT=$(pwd) go install github.com/wailsapp/wails/v2/cmd/wails@latest go mod tidy # 安装前端依赖 cd ${ROOT}/frontend npm install # 安装后端环境 cd ${ROOT}/thirdparty pip install -r requirements.txt pyinstaller -F -w pdf.py mkdir ${ROOT}/build/bin # 1) for darwin, linux cp dist/pdf ocr.py convert.py ${ROOT}/build/bin # 2) for windows cp dist/pdf.exe ${ROOT}/build/bin cp ocr.py ${ROOT}/build/bin cp convert.py ${ROOT}/build/bin cd $ROOT wails dev # 开发预览 wails build # 编译将build/bin目录打包,运行PDF Guru即可。最后,想学习这个项目的可以查看 项目地址 !!
2023年08月14日
47 阅读
0 评论
0 点赞
2023-07-24
SpringBoot 项目使用 Redis 对用户 IP 进行接口限流
一、思路使用接口限流的主要目的在于提高系统的稳定性,防止接口被恶意打击(短时间内大量请求)。比如要求某接口在1分钟内请求次数不超过1000次,那么应该如何设计代码呢?下面讲两种思路,如果想看代码可直接翻到后面的代码部分。1.1 固定时间段(旧思路)1.1.1 思路描述 该方案的思路是:使用Redis记录固定时间段内某用户IP访问某接口的次数,其中:Redis的key:用户IP + 接口方法名Redis的value:当前接口访问次数。当用户在近期内第一次访问该接口时,向Redis中设置一个包含了用户IP和接口方法名的key,value的值初始化为1(表示第一次访问当前接口)。同时,设置该key的过期时间(比如为60秒)。之后,只要这个key还未过期,用户每次访问该接口都会导致value自增1次。用户每次访问接口前,先从Redis中拿到当前接口访问次数,如果发现访问次数大于规定的次数(如超过1000次),则向用户返回接口访问失败的标识。1.1.2 思路缺陷 该方案的缺点在于,限流时间段是固定的。比如要求某接口在1分钟内请求次数不超过1000次,观察以下流程:可以发现,00:59和01:01之间仅仅间隔了2秒,但接口却被访问了1000+999=1999次,是限流次数(1000次)的2倍!所以在该方案中,限流次数的设置可能不起作用,仍然可能在短时间内造成大量访问。1.2 滑动窗口(新思路)1.2.1 思路描述 为了避免出现方案1中由于键过期导致的短期访问量增大的情况,我们可以改变一下思路,也就是把固定的时间段改成动态的:假设某个接口在10秒内只允许访问5次。用户每次访问接口时,记录当前用户访问的时间点(时间戳),并计算前10秒内用户访问该接口的总次数。如果总次数大于限流次数,则不允许用户访问该接口。这样就能保证在任意时刻用户的访问次数不会超过1000次。如下图,假设用户在0:19时间点访问接口,经检查其前10秒内访问次数为5次,则允许本次访问。假设用户0:20时间点访问接口,经检查其前10秒内访问次数为6次(超出限流次数5次),则不允许本次访问。1.2.2 Redis部分的实现1)选用何种 Redis 数据结构首先是需要确定使用哪个Redis数据结构。用户每次访问时,需要用一个key记录用户访问的时间点,而且还需要利用这些时间点进行范围检查。为何选择 zSet 数据结构为了能够实现范围检查,可以考虑使用Redis中的zSet有序集合。添加一个zSet元素的命令如下:ZADD [key] [score] [member]它有一个关键的属性score,通过它可以记录当前member的优先级。于是我们可以把score设置成用户访问接口的时间戳,以便于通过score进行范围检查。key则记录用户IP和接口方法名,至于member设置成什么没有影响,一个member记录了用户访问接口的时间点。因此member也可以设置成时间戳。3)zSet 如何进行范围检查(检查前几秒的访问次数)思路是,把特定时间间隔之前的member都删掉,留下的member就是时间间隔之内的总访问次数。然后统计当前key中的member有多少个即可。① 把特定时间间隔之前的member都删掉。zSet有如下命令,用于删除score范围在[min~max]之间的member:Zremrangebyscore [key] [min] [max]假设限流时间设置为5秒,当前用户访问接口时,获取当前系统时间戳为currentTimeMill,那么删除的score范围可以设置为:min = 0 max = currentTimeMill - 5 * 1000相当于把5秒之前的所有member都删除了,只留下前5秒内的key。② 统计特定key中已存在的member有多少个。zSet有如下命令,用于统计某个key的member总数: ZCARD [key]统计的key的member总数,就是当前接口已经访问的次数。如果该数目大于限流次数,则说明当前的访问应被限流。二、代码实现主要是使用注解 + AOP的形式实现。2.1 固定时间段思路使用了lua脚本。参考:https://blog.csdn.net/qq_43641418/article/details/1277644622.1.1 限流注解@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface RateLimiter { /** * 限流时间,单位秒 */ int time() default 5; /** * 限流次数 */ int count() default 10; }2.1.2 定义lua脚本 在 resources/lua 下新建 limit.lua :-- 获取redis键 local key = KEYS[1] -- 获取第一个参数(次数) local count = tonumber(ARGV[1]) -- 获取第二个参数(时间) local time = tonumber(ARGV[2]) -- 获取当前流量 local current = redis.call('get', key); -- 如果current值存在,且值大于规定的次数,则拒绝放行(直接返回当前流量) if current and tonumber(current) > count then return tonumber(current) end -- 如果值小于规定次数,或值不存在,则允许放行,当前流量数+1 (值不存在情况下,可以自增变为1) current = redis.call('incr', key); -- 如果是第一次进来,那么开始设置键的过期时间。 if tonumber(current) == 1 then redis.call('expire', key, time); end -- 返回当前流量 return tonumber(current)2.1.3 注入Lua执行脚本 关键代码是 limitScript() 方法@Configuration public class RedisConfig { @Bean public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(connectionFactory); // 使用Jackson2JsonRedisSerialize 替换默认序列化(默认采用的是JDK序列化) Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class); ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(om); redisTemplate.setKeySerializer(jackson2JsonRedisSerializer); redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); redisTemplate.setHashKeySerializer(jackson2JsonRedisSerializer); redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer); return redisTemplate; } /** * 解析lua脚本的bean */ @Bean("limitScript") public DefaultRedisScript<Long> limitScript() { DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(); redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/limit.lua"))); redisScript.setResultType(Long.class); return redisScript; } }2.1.3 定义Aop切面类@Slf4j @Aspect @Component public class RateLimiterAspect { @Autowired private RedisTemplate redisTemplate; @Autowired private RedisScript<Long> limitScript; @Before("@annotation(rateLimiter)") public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable { int time = rateLimiter.time(); int count = rateLimiter.count(); String combineKey = getCombineKey(rateLimiter.type(), point); List<String> keys = Collections.singletonList(combineKey); try { Long number = (Long) redisTemplate.execute(limitScript, keys, count, time); // 当前流量number已超过限制,则抛出异常 if (number == null || number.intValue() > count) { throw new RuntimeException("访问过于频繁,请稍后再试"); } log.info("[limit] 限制请求数'{}',当前请求数'{}',缓存key'{}'", count, number.intValue(), combineKey); } catch (Exception ex) { ex.printStackTrace(); throw new RuntimeException("服务器限流异常,请稍候再试"); } } /** * 把用户IP和接口方法名拼接成 redis 的 key * @param point 切入点 * @return 组合key */ private String getCombineKey(JoinPoint point) { StringBuilder sb = new StringBuilder("rate_limit:"); ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); sb.append( Utils.getIpAddress(request) ); MethodSignature signature = (MethodSignature) point.getSignature(); Method method = signature.getMethod(); Class<?> targetClass = method.getDeclaringClass(); // keyPrefix + "-" + class + "-" + method return sb.append("-").append( targetClass.getName() ) .append("-").append(method.getName()).toString(); } }2.2 滑动窗口思路2.2.1 限流注解@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface RateLimiter { /** * 限流时间,单位秒 */ int time() default 5; /** * 限流次数 */ int count() default 10; }2.2.2 定义Aop切面类@Slf4j @Aspect @Component public class RateLimiterAspect { @Autowired private RedisTemplate redisTemplate; /** * 实现限流(新思路) * @param point * @param rateLimiter * @throws Throwable */ @SuppressWarnings("unchecked") @Before("@annotation(rateLimiter)") public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable { // 在 {time} 秒内仅允许访问 {count} 次。 int time = rateLimiter.time(); int count = rateLimiter.count(); // 根据用户IP(可选)和接口方法,构造key String combineKey = getCombineKey(rateLimiter.type(), point); // 限流逻辑实现 ZSetOperations zSetOperations = redisTemplate.opsForZSet(); // 记录本次访问的时间结点 long currentMs = System.currentTimeMillis(); zSetOperations.add(combineKey, currentMs, currentMs); // 这一步是为了防止member一直存在于内存中 redisTemplate.expire(combineKey, time, TimeUnit.SECONDS); // 移除{time}秒之前的访问记录(滑动窗口思想) zSetOperations.removeRangeByScore(combineKey, 0, currentMs - time * 1000); // 获得当前窗口内的访问记录数 Long currCount = zSetOperations.zCard(combineKey); // 限流判断 if (currCount > count) { log.error("[limit] 限制请求数'{}',当前请求数'{}',缓存key'{}'", count, currCount, combineKey); throw new RuntimeException("访问过于频繁,请稍后再试!"); } } /** * 把用户IP和接口方法名拼接成 redis 的 key * @param point 切入点 * @return 组合key */ private String getCombineKey(JoinPoint point) { StringBuilder sb = new StringBuilder("rate_limit:"); ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); sb.append( Utils.getIpAddress(request) ); MethodSignature signature = (MethodSignature) point.getSignature(); Method method = signature.getMethod(); Class<?> targetClass = method.getDeclaringClass(); // keyPrefix + "-" + class + "-" + method return sb.append("-").append( targetClass.getName() ) .append("-").append(method.getName()).toString(); } }
2023年07月24日
45 阅读
0 评论
0 点赞
2023-07-21
Flex 布局教程:语法篇
Flex 布局教程:语法篇作者: 阮一峰日期: 2015年7月10日网页布局(layout)是 CSS 的一个重点应用。布局的传统解决方案,基于盒状模型,依赖 display 属性 + position属性 + float属性。它对于那些特殊布局非常不方便,比如,垂直居中就不容易实现。2009年,W3C 提出了一种新的方案----Flex 布局,可以简便、完整、响应式地实现各种页面布局。目前,它已经得到了所有浏览器的支持,这意味着,现在就能很安全地使用这项功能。Flex 布局将成为未来布局的首选方案。本文介绍它的语法,下一篇文章给出常见布局的 Flex 写法。网友 JailBreak 为本文的所有示例制作了 Demo,也可以参考。以下内容主要参考了下面两篇文章:A Complete Guide to Flexbox 和 A Visual Guide to CSS3 Flexbox Properties。一、Flex 布局是什么?Flex 是 Flexible Box 的缩写,意为"弹性布局",用来为盒状模型提供最大的灵活性。任何一个容器都可以指定为 Flex 布局。 .box{ display: flex; } 行内元素也可以使用 Flex 布局。 .box{ display: inline-flex; } Webkit 内核的浏览器,必须加上-webkit前缀。 .box{ display: -webkit-flex; /* Safari */ display: flex; } 注意,设为 Flex 布局以后,子元素的float、clear和vertical-align属性将失效。二、基本概念采用 Flex 布局的元素,称为 Flex 容器(flex container),简称"容器"。它的所有子元素自动成为容器成员,称为 Flex 项目(flex item),简称"项目"。容器默认存在两根轴:水平的主轴(main axis)和垂直的交叉轴(cross axis)。主轴的开始位置(与边框的交叉点)叫做main start,结束位置叫做main end;交叉轴的开始位置叫做cross start,结束位置叫做cross end。项目默认沿主轴排列。单个项目占据的主轴空间叫做main size,占据的交叉轴空间叫做cross size。三、容器的属性以下6个属性设置在容器上。flex-directionflex-wrapflex-flowjustify-contentalign-itemsalign-content3.1 flex-direction属性flex-direction属性决定主轴的方向(即项目的排列方向)。 .box { flex-direction: row | row-reverse | column | column-reverse; } 它可能有4个值。row(默认值):主轴为水平方向,起点在左端。row-reverse:主轴为水平方向,起点在右端。column:主轴为垂直方向,起点在上沿。column-reverse:主轴为垂直方向,起点在下沿。3.2 flex-wrap属性默认情况下,项目都排在一条线(又称"轴线")上。flex-wrap属性定义,如果一条轴线排不下,如何换行。 .box{ flex-wrap: nowrap | wrap | wrap-reverse; } 它可能取三个值。(1)nowrap(默认):不换行。(2)wrap:换行,第一行在上方。(3)wrap-reverse:换行,第一行在下方。3.3 flex-flowflex-flow属性是flex-direction属性和flex-wrap属性的简写形式,默认值为row nowrap。 .box { flex-flow: <flex-direction> || <flex-wrap>; } 3.4 justify-content属性justify-content属性定义了项目在主轴上的对齐方式。 .box { justify-content: flex-start | flex-end | center | space-between | space-around; } 它可能取5个值,具体对齐方式与轴的方向有关。下面假设主轴为从左到右。flex-start(默认值):左对齐flex-end:右对齐center: 居中space-between:两端对齐,项目之间的间隔都相等。space-around:每个项目两侧的间隔相等。所以,项目之间的间隔比项目与边框的间隔大一倍。3.5 align-items属性align-items属性定义项目在交叉轴上如何对齐。 .box { align-items: flex-start | flex-end | center | baseline | stretch; } 它可能取5个值。具体的对齐方式与交叉轴的方向有关,下面假设交叉轴从上到下。flex-start:交叉轴的起点对齐。flex-end:交叉轴的终点对齐。center:交叉轴的中点对齐。baseline: 项目的第一行文字的基线对齐。stretch(默认值):如果项目未设置高度或设为auto,将占满整个容器的高度。3.6 align-content属性align-content属性定义了多根轴线的对齐方式。如果项目只有一根轴线,该属性不起作用。 .box { align-content: flex-start | flex-end | center | space-between | space-around | stretch; } 该属性可能取6个值。flex-start:与交叉轴的起点对齐。flex-end:与交叉轴的终点对齐。center:与交叉轴的中点对齐。space-between:与交叉轴两端对齐,轴线之间的间隔平均分布。space-around:每根轴线两侧的间隔都相等。所以,轴线之间的间隔比轴线与边框的间隔大一倍。stretch(默认值):轴线占满整个交叉轴。四、项目的属性以下6个属性设置在项目上。orderflex-growflex-shrinkflex-basisflexalign-self4.1 order属性order属性定义项目的排列顺序。数值越小,排列越靠前,默认为0。 .item { order: <integer>; } 4.2 flex-grow属性flex-grow属性定义项目的放大比例,默认为0,即如果存在剩余空间,也不放大。 .item { flex-grow: <number>; /* default 0 */ } 如果所有项目的flex-grow属性都为1,则它们将等分剩余空间(如果有的话)。如果一个项目的flex-grow属性为2,其他项目都为1,则前者占据的剩余空间将比其他项多一倍。4.3 flex-shrink属性flex-shrink属性定义了项目的缩小比例,默认为1,即如果空间不足,该项目将缩小。 .item { flex-shrink: <number>; /* default 1 */ } 如果所有项目的flex-shrink属性都为1,当空间不足时,都将等比例缩小。如果一个项目的flex-shrink属性为0,其他项目都为1,则空间不足时,前者不缩小。负值对该属性无效。4.4 flex-basis属性flex-basis属性定义了在分配多余空间之前,项目占据的主轴空间(main size)。浏览器根据这个属性,计算主轴是否有多余空间。它的默认值为auto,即项目的本来大小。 .item { flex-basis: <length> | auto; /* default auto */ } 它可以设为跟width或height属性一样的值(比如350px),则项目将占据固定空间。4.5 flex属性flex属性是flex-grow, flex-shrink 和 flex-basis的简写,默认值为0 1 auto。后两个属性可选。 .item { flex: none | [ <'flex-grow'> <'flex-shrink'>? || <'flex-basis'> ] } 该属性有两个快捷值:auto (1 1 auto) 和 none (0 0 auto)。建议优先使用这个属性,而不是单独写三个分离的属性,因为浏览器会推算相关值。4.6 align-self属性align-self属性允许单个项目有与其他项目不一样的对齐方式,可覆盖align-items属性。默认值为auto,表示继承父元素的align-items属性,如果没有父元素,则等同于stretch。 .item { align-self: auto | flex-start | flex-end | center | baseline | stretch; } 该属性可能取6个值,除了auto,其他都与align-items属性完全一致。(完)
2023年07月21日
19 阅读
0 评论
0 点赞
1
...
10
11
12
...
27