-
Notifications
You must be signed in to change notification settings - Fork 0
/
search.json
1 lines (1 loc) · 115 KB
/
search.json
1
[{"title":"Composer 提示 Allowed memory size of bytes exhausted","url":"/2021/04/c8824c4f5063/","content":"运行 composer 命令时提示:\n\nPHP Fatal error: Allowed memory size of 1610612736 bytes exhausted (tried to allocate 67108864 bytes)\n\n修改 php.ini 中 memory_limit 的值为 -1,重启\n执行 php -r "echo ini_get('memory_limit').PHP_EOL;" 查看是否修改成功\n","categories":["错误记录"],"tags":["Composer"]},{"title":"什么是 CSRF","url":"/2020/12/f659e8d93e88/","content":"一.CSRF是什么?是什么?是什么?是什么?是什么?是什么?CSRF(Cross-site request forgery),中文名称:跨站请求伪造,也被称为:one click attack/session riding,缩写为:CSRF/XSRF。\n二.CSRF可以做什么?你这可以这么理解CSRF攻击:攻击者盗用了你的身份,以你的名义发送恶意请求。CSRF能够做的事情包括:以你名义发送邮件,发消息,盗取你的账号,甚至于购买商品,虚拟货币转账……造成的问题包括:个人隐私泄露以及财产安全。\n三.CSRF漏洞现状CSRF这种攻击方式在2000年已经被国外的安全人员提出,但在国内,直到06年才开始被关注,08年,国内外的多个大型社区和交互网站分别爆出CSRF漏洞,如:NYTimes.com(纽约时报)、Metafilter(一个大型的BLOG网站),YouTube和百度HI……而现在,互联网上的许多站点仍对此毫无防备,以至于安全业界称CSRF为“沉睡的巨人”。\n四.CSRF的原理下图简单阐述了CSRF攻击的思想:\n\n从上图可以看出,要完成一次CSRF攻击,受害者必须依次完成两个步骤:\n1.登录受信任网站A,并在本地生成Cookie。\n2.在不登出A的情况下,访问危险网站B。\n看到这里,你也许会说:“如果我不满足以上两个条件中的一个,我就不会受到CSRF的攻击”。是的,确实如此,但你不能保证以下情况不会发生:\n1.你不能保证你登录了一个网站后,不再打开一个tab页面并访问另外的网站。\n2.你不能保证你关闭浏览器了后,你本地的Cookie立刻过期,你上次的会话已经结束。(事实上,关闭浏览器不能结束一个会话,但大多数人都会错误的认为关闭浏览器就等于退出登录/结束会话了……)\n3.上图中所谓的攻击网站,可能是一个存在其他漏洞的可信任的经常被人访问的网站。\n上面大概地讲了一下CSRF攻击的思想,下面我将用几个例子详细说说具体的CSRF攻击,这里我以一个银行转账的操作作为例子(仅仅是例子,真实的银行网站没这么傻:>)\n示例1:\n银行网站A,它以GET请求来完成银行转账的操作,如:http://www.mybank.com/Transfer.php?toBankId=11&money=1000\n危险网站B,它里面有一段HTML的代码如下:\nhttp://www.mybank.com/Transfer.php?toBankId=11&money=1000\n\n首先,你登录了银行网站A,然后访问危险网站B,噢,这时你会发现你的银行账户少了1000块……\n为什么会这样呢?原因是银行网站A违反了HTTP规范,使用GET请求更新资源。在访问危险网站B的之前,你已经登录了银行网站A,而B中的以GET的方式请求第三方资源(这里的第三方就是指银行网站了,原本这是一个合法的请求,但这里被不法分子利用了),所以你的浏览器会带上你的银行网站A的Cookie发出Get请求,去获取资源“ http://www.mybank.com/Transfer.php?toBankId=11&money=1000”,结果银行网站服务器收到请求后,认为这是一个更新资源操作(转账操作),所以就立刻进行转账操作……\n示例2:\n为了杜绝上面的问题,银行决定改用POST请求完成转账操作。\n银行网站A的WEB表单如下:\n <form action="Transfer.php" method="POST"> <p>ToBankId: <input type="text" name="toBankId" /></p> <p>Money: <input type="text" name="money" /></p> <p><input type="submit" value="Transfer" /></p> </form>\n\n\n\n后台处理页面Transfer.php如下:\n<?php session_start(); if (isset($_REQUEST['toBankId'] && isset($_REQUEST['money'])) { buy_stocks($_REQUEST['toBankId'], $_REQUEST['money']); }?>\n\n\n\n危险网站B,仍然只是包含那句HTML代码:\n<img src=http://www.mybank.com/Transfer.php?toBankId=11&money=1000>\n\n\n\n和示例1中的操作一样,你首先登录了银行网站A,然后访问危险网站B,结果…..和示例1一样,你再次没了1000块~T_T,这次事故的原因是:银行后台使用了$_REQUEST去获取请求的数据,而$_REQUEST既可以获取GET请求的数据,也可以获取POST请求的数据,这就造成了在后台处理程序无法区分这到底是GET请求的数据还是POST请求的数据。在PHP中,可以使用$_GET和$_POST分别获取GET请求和POST请求的数据。在JAVA中,用于获取请求数据request一样存在不能区分GET请求数据和POST数据的问题。\n示例3:\n经过前面2个惨痛的教训,银行决定把获取请求数据的方法也改了,改用$_POST,只获取POST请求的数据,后台处理页面Transfer.php代码如下:\n<?php session_start(); if (isset($_POST['toBankId'] && isset($_POST['money'])) { buy_stocks($_POST['toBankId'], $_POST['money']); } ?>\n\n然而,危险网站B与时俱进,它改了一下代码:\n<html> <head> <script type="text/javascript"> function steal() { iframe = document.frames["steal"]; iframe.document.Submit("transfer"); } </script> </head> <body onload="steal()"> <iframe name="steal" display="none"> <form method="POST" name="transfer" action="http://www.myBank.com/Transfer.php"> <input type="hidden" name="toBankId" value="11"> <input type="hidden" name="money" value="1000"> </form> </iframe> </body></html>\n\n\n\n如果用户仍是继续上面的操作,很不幸,结果将会是再次不见1000块……因为这里危险网站B暗地里发送了POST请求到银行!\n总结一下上面3个例子,CSRF主要的攻击模式基本上是以上的3种,其中以第1,2种最为严重,因为触发条件很简单,一个就可以了,而第3种比较麻烦,需要使用JavaScript,所以使用的机会会比前面的少很多,但无论是哪种情况,只要触发了CSRF攻击,后果都有可能很严重。\n理解上面的3种攻击模式,其实可以看出,CSRF攻击是源于WEB的隐式身份验证机制!WEB的身份验证机制虽然可以保证一个请求是来自于某个用户的浏览器,但却无法保证该请求是用户批准发送的!\n五.CSRF的防御我总结了一下看到的资料,CSRF的防御可以从服务端和客户端两方面着手,防御效果是从服务端着手效果比较好,现在一般的CSRF防御也都在服务端进行。\n1.服务端进行CSRF防御\n服务端的CSRF方式方法很多样,但总的思想都是一致的,就是在客户端页面增加伪随机数。\n(1).Cookie Hashing(所有表单都包含同一个伪随机值):\n这可能是最简单的解决方案了,因为攻击者不能获得第三方的Cookie(理论上),所以表单中的数据也就构造失败了:\n<?php //构造加密的Cookie信息 $value = “DefenseSCRF”; setcookie(”cookie”, $value, time()+3600); ?>\n\n\n\n在表单里增加Hash值,以认证这确实是用户发送的请求。\n <?php $hash = md5($_COOKIE['cookie']); ?> <form method=”POST” action=”transfer.php”> <input type=”text” name=”toBankId”> <input type=”text” name=”money”> <input type=”hidden” name=”hash” value=”<?=$hash;?>”> <input type=”submit” name=”submit” value=”Submit”> </form>\n\n\n\n然后在服务器端进行Hash值验证\n<?php if(isset($_POST['check'])) { $hash = md5($_COOKIE['cookie']); if($_POST['check'] == $hash) { doJob(); } else { //... } } else { //... } ?>\n\n\n\n这个方法个人觉得已经可以杜绝99%的CSRF攻击了,那还有1%呢….由于用户的Cookie很容易由于网站的XSS漏洞而被盗取,这就另外的1%。一般的攻击者看到有需要算Hash值,基本都会放弃了,某些除外,所以如果需要100%的杜绝,这个不是最好的方法。(2).验证码\n这个方案的思路是:每次的用户提交都需要用户在表单中填写一个图片上的随机字符串,厄….这个方案可以完全解决CSRF,但个人觉得在易用性方面似乎不是太好,还有听闻是验证码图片的使用涉及了一个被称为MHTML的Bug,可能在某些版本的微软IE中受影响。\n(3).One-Time Tokens(不同的表单包含一个不同的伪随机值)\n在实现One-Time Tokens时,需要注意一点:就是“并行会话的兼容”。如果用户在一个站点上同时打开了两个不同的表单,CSRF保护措施不应该影响到他对任何表单的提交。考虑一下如果每次表单被装入时站点生成一个伪随机值来覆盖以前的伪随机值将会发生什么情况:用户只能成功地提交他最后打开的表单,因为所有其他的表单都含有非法的伪随机值。必须小心操作以确保CSRF保护措施不会影响选项卡式的浏览或者利用多个浏览器窗口浏览一个站点。\n以下我的实现:\n1).先是令牌生成函数(gen_token()):\n<?php function gen_token() { //这里我是贪方便,实际上单使用Rand()得出的随机数作为令牌,也是不安全的。 //这个可以参考我写的Findbugs笔记中的《Random object created and used only once》 $token = md5(uniqid(rand(), true)); return $token; }\n\n\n\n2).然后是Session令牌生成函数(gen_stoken()):\n<?php function gen_stoken() { $pToken = ""; if($_SESSION[STOKEN_NAME] == $pToken){ //没有值,赋新值 $_SESSION[STOKEN_NAME] = gen_token(); } else{ //继续使用旧的值 } } ?>\n\n\n\n3).WEB表单生成隐藏输入域的函数:\n<?php function gen_input() { gen_stoken(); echo “<input type=\\”hidden\\” name=\\”" . FTOKEN_NAME . “\\” value=\\”" . $_SESSION[STOKEN_NAME] . “\\”> “; } ?>\n\n\n\n4).WEB表单结构:\n<?php session_start(); include(”functions.php”); ?> <form method=”POST” action=”transfer.php”> <input type=”text” name=”toBankId”> <input type=”text” name=”money”> <? gen_input(); ?> <input type=”submit” name=”submit” value=”Submit”> </FORM>\n\n\n\n5).服务端核对令牌\n","categories":["网络"]},{"title":"Docker 常用命令","url":"/2021/07/b934f7bfb536/","content":"\ndocker [command] [options]\nattach # 当前 shell 下 attach 连接指定运行镜像build # 通过 Dockerfile 定制镜像commit # 提交当前容器为新的镜像cp # 从容器中拷贝指定文件或者目录带宿主机中create # 创建一个新的容器,同 run ,但不启动容器diff # 查看 docker 容器变化events # 从 docker 服务获取容器实时事件exec # 在已存在的容器上运行命令export # 导出容器的内容流作为一个 tar 归档文件[对应 import]history # 展示一个镜像形成的历史images # 列出系统当前镜像import # 从 tar 包中的内容创建一个新的文件系统映像[对应 export]info # 显示系统相关信息inspect # 查看容器详细信息kill # kill 指定 docker 容器load # 从一个 tar 包中加载一个镜像login # 注册或者登陆一个 docker 源服务器logout # 从当前 Docker registry 退出logs # 输出当前容器日志信息port # 查看映射端口对应容器内部源端口pause # 暂停容器ps # 列出容器列表pull # 从 docker 镜像源服务器拉取指定镜像或者库镜像push # 推送指定镜像或者库镜像至 docker 源服务器restart # 重启运行的容器rm # 移除一个或者多个容器rmi # 移除一个或者多个镜像[无容器使用该镜像才可删除,否则需删除相关内容才可以继续或 -f 强制删除]run # 创建一个新的容器并运行一个命令save # 保证一个镜像为一个 tar 包[对应 load]search # 在 docker hub 中搜索镜像start # 启动容器stop # 停止容器tag # 给源中镜像打标签top # 查看容器中运行的进程信息unpause # 取消暂停容器version # 查看 docker 版本号wait # 截取容器停止时的退出状态值\n\n\n参考- 遇见狂神说","tags":["docker"]},{"title":"Laravel 中优化 app.js 的大小","url":"/2021/10/30fc9eddc78b/","content":"\n近期做了一个小项目,前端是通过 Laravel Mix 编译的,产生的 app.js ,大小有 3M 还多,不得不优化一下……\n\n没做任何优化之前:\n\n查看应用的前端库结构\n安装 npm install --save-dev webpack-bundle-analyzer 。\n\n修改 webpack.mix.js 配置 。\nconst BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;mix.webpackConfig({ plugins: [ new BundleAnalyzerPlugin(), ]});\n运行 npm run dev 或者 npm run production ,浏览器中自动打开 http://127.0.0.1:8888/ 页面。\n鼠标悬浮可以显示包大小\n\n\n\n分离内存较大的库这些库会被 webpack 一起打包成一个 app.js,所以减小应用体积的最有效方法就是将这些库分离出去,别将它们与我们自己写的应用代码打包到一起。大部分有名的第三方库在公共CDN都有存放,如 bootcss, unpkg 等,速度飞快,可以直接引用。\n\n配置 webpack.mix.js :\nmix.webpackConfig({ plugins: [ new BundleAnalyzerPlugin(), ], externals: { 'element-ui': 'Element', 'vue': 'Vue', 'lodash': '_', }}).js('resources/js/app.js', 'public/js') .sass('resources/sass/app.scss', 'public/css');\nresources/views/layouts/app.blade.php 文件中加入 element-ui、vue、lodash 的外链\n<link href="https://cdn.bootcss.com/element-ui/2.0.11/theme-chalk/index.css" rel="stylesheet"><script src="//cdn.bootcss.com/vue/2.5.13/vue.min.js"></script><script src="//cdn.bootcss.com/element-ui/2.0.11/index.js"></script><script src="//cdn.bootcss.com/lodash.js/4.17.4/lodash.min.js"></script>\n resources/js/app.js 和 resources/js/bootstrap.js 文件中将 require('vue') 、require('lodash') 、import ElementUI from 'element-ui' 的位置删除,否则编译时会报错。\n\n运行 npm run dev\n\n运行 npm run production\n\n\n优化浏览器加载时的大小在这里要做的就是开启 Nginx 的 gzip 压缩 。\n\n没开启时:\n开启 gzip 压缩后\n\nnginx.conf 中配置:\n#gzip on;gzip on;gzip_min_length 1k; # 不压缩临界值,大于1K的才压缩,一般不用改gzip_buffers 4 32k; # 设置用于处理请求压缩的缓冲区数量和大小。比如32 4K表示按照内存页(one memory page)大小以4K为单位(即一个系统中内存页为4K),申请32倍的内存空间。建议此项不设置,使用默认值。gzip_http_version 1.1; gzip_comp_level 2; # 压缩级别,1-10,数字越大压缩的越好,时间也越长,看心情随便改吧gzip_types text/plain application/x-javascript text/css application/xml; # 进行压缩的文件类型gzip_vary on; # 跟Squid等缓存服务有关,on的话会在Header里增加"Vary: Accept-Encoding",按需开启gzip_disable "MSIE [1-6].";\n\n\n\n参考- Laravel框架中缩小Vue应用的体积\n\n\n\n\n\n","categories":["Laravel"],"tags":["Laravel","Laravel-Mix","webpack"]},{"title":"Linux 内存进程命令","url":"/2021/07/d8af1e4291bf/","content":"top常用的性能分析工具,能够实时显示系统中各个进程的资源占用状况\nvagrant@homestead:~$ top -help procps-ng 3.3.12Usage: top -hv | -bcHiOSs -d secs -n max -u|U user -p pid(s) -o field -w [cols] vagrant@homestead:~$ top\n\n\nPID: 进程的IDUSER: 进程所有者PR: 进程的优先级别,越小越优先被执行NInice: 值VIRT: 进程占用的虚拟内存RES: 进程占用的物理内存SHR: 进程使用的共享内存S: 进程的状态。S表示休眠,R表示正在运行,Z表示僵死状态,N表示该进程优先值为负数%CPU: 进程占用CPU的使用率%MEM: 进程使用的物理内存和总内存的百分比TIME+: 该进程启动后占用的总的CPU时间,即占用CPU使用时间的累加值。COMMAND:进程启动命令名称\n\n","categories":["Linux"],"tags":["linux","top","linux进程"]},{"title":"MongoDB 运算符","url":"/2021/08/b4b49ce1fe51/","content":"查询\n比较\n\n\n\n\n名称\n描述\n\n\n\n$eq\n匹配 等于 指定值的值\n\n\n$gt\n匹配 大于 指定值的值\n\n\n$gte\n匹配 大于或等于 指定值的值\n\n\n$in\n匹配 数组中 指定的任何值\n\n\n$lt\n匹配 小于 指定值的值\n\n\n$lte\n匹配 小于或等于 指定值的值\n\n\n$ne\n匹配 所有不等于 指定值的值\n\n\n$nin\n不匹配数组中指定的任何值\n\n\n\n逻辑\n\n\n\n\n名称\n描述\n\n\n\n$and\n将查询子句与逻辑 AND 连接,返回匹配这两个子句条件的所有文档\n\n\n$not\n反转查询表达式的效果,并返回与查询表达式不匹配的文档\n\n\n$nor\n将查询子句与逻辑 NOR 连接,返回两个子句都不匹配的所有文档。\n\n\n$or\n将查询子句与逻辑 OR 连接,返回与任一子句条件匹配的所有文档\n\n\n\n元素\n\n\n\n\n名称\n描述\n\n\n\n$exists\n匹配具有指定字段的文档\n\n\n$type\n如果字段为指定类型,则选择文档\n\n\n更新\n字段\n\n\n\n\n名称\n描述\n\n\n\n$currentDate\n将字段的值设置为当前日期,作为日期或时间戳\n\n\n$inc\n将字段的值增加指定的数量\n\n\n$min\n仅当指定值小于现有字段值时才更新该字段\n\n\n$max\n仅当指定值大于现有字段值时才更新该字段\n\n\n$mul\n将字段的值乘以指定的数量\n\n\n$rename\n重命名字段\n\n\n$set\n设置文档中字段的值\n\n\n$setOnInsert\n如果更新导致插入文档,则设置字段的值。对修改现有文档的更新操作没有影响\n\n\n$unset\n从文档中删除指定的字段\n\n\ntest> db.inventory.updateOne(... {item:"paper"},... {..... $set: {"size.uom":"cm", status:"p"},..... $currentDate: {lastModified: true}..... }... ){ acknowledged: true, insertedId: null, matchedCount: 1, modifiedCount: 1, upsertedCount: 0}\n\n\n数组\n\n\n\n\n名称\n描述\n\n\n\n$\n充当占位符以更新与查询条件匹配的第一个元素\n\n\n$[]\n充当占位符,为匹配查询条件的文档更新数组中的所有元素\n\n\n$[<identifier>]\n充当占位符,arrayFilters为符合查询条件的文档更新符合条件的所有元素\n\n\n$addToSet\n仅当集合中尚不存在元素时,才将元素添加到数组中\n\n\n$pop\n删除数组的第一项或最后一项\n\n\n$pull\n删除与指定查询匹配的所有数组元素\n\n\n$push\n将项目添加到数组\n\n\n$pullAll\n从数组中删除所有匹配的值\n\n\n","categories":["MongoDB"],"tags":["MongoDB"]},{"title":"MongoDB 地理空间","url":"/2021/08/8c38ef739811/","content":"GeoJSON 对象<field>: { type: <GeoJSON type> , # 指定的 GeoJSON 类型 (Point、LineString、Polygon……) coordinates: <coordinates> # 指定对象坐标的字段 [<longitude>, <latitude> ]} # 例:location: { type: "Point", coordinates: [ 116.408, 39.904]}\n\n传统坐标对# 数组型<field>: [ <x>, <y> ] # 或<field>: [<longitude>, <latitude> ]# 或 嵌入文档型<field>: { <field1>: <longitude>, <field2>: <latitude> }# 要指定旧坐标对,数组优先于嵌入文档,因为某些语言不保证关联地图排序\n\n地理空间索引\n2dsphere\n\n2dsphere 索引支持在类地球体上计算几何图形的查询\n# 创建 2dsphere 索引:db.collection.createIndex( { <location field> : "2dsphere" } )\n\n其中 <location field> 是一个字段,其值为 GeoJSON 对象或传统坐标对\n\n2d\n\n2d索引支持在二维平面上计算几何的查询, 可以支持 $nearSphere 在球面上计算的查询。(适用于 MongoDB 2.2 及更早版本)\ndb.collection.createIndex( { <location field> : "2d" } )\n\n其中 <location field> 是一个字段,其值为传统坐标对。\n地理空间查询运算符\n\n\n名称\n描述\n\n\n\n$geoIntersects\n选择与 GeoJSON 几何相交的几何。 2dsphere 索引支持 $geoIntersects。\n\n\n$geoWithin\n在边界GeoJSON 几何中选择几何。 2dsphere 和 2d 指标支持 $geoWithin。\n\n\n$near\n返回点附近的地理空间对象。需要地理空间索引。 2dsphere 和 2d 指标支持 $near。\n\n\n$nearSphere\n返回球体上某个点附近的地理空间对象。需要地理空间索引。 2dsphere 和 2d 指标支持 $nearSphere。\n\n\n地理空间聚合阶段\n$geoNear\n\n按离指定点最近到最远的顺序输出文档。\n{ $geoNear: { <geoNear options> } }\n\n\n\n\n字段\n类型\n描述\n\n\n\ndistanceField\nstring\n包含计算距离的输出字段\n\n\ndistanceMultiplier\nnumber\n可选的。乘以查询返回的所有距离的因子。例如,使用 distanceMultiplier 将球面查询返回的弧度乘以地球的半径来转换为公里\n\n\nincludeLocs\nstring\n可选的。这指定了标识用于计算距离的位置的输出字段。当位置字段包含多个位置时,此选项很有用\n\n\nkey\n\n可选的。指定计算距离时要使用的地理空间索引字段。如果您的集合具有多个2d和/或多个2dsphere 索引,则必须使用该key选项来指定要使用的索引字段路径\n\n\nmaxDistance\nnumber\n可选的。到中心点的最大距离\n\n\nminDistance\nnumber\n可选的。到中心点的最小距离\n\n\nnear\nGeoJSON 或 传统坐标对\n查找最近文档的点\n\n\nquery\ndocument\n查询的匹配条件\n\n\nspherical\nboolean\n可选的。确定 MongoDB 如何计算两点之间的距离。true:MongoDB 使用 $nearSphere 语义并使用球面几何计算距离**\n\n\nfalse**:MongoDB 使用 $near 语义:2dsphere 索引的球面几何和 2d 索引的平面几何\n\n\n\n\nuniqueDocs\nboolean\n可选的。如果此值为true,则查询会返回匹配的文档一次,即使文档的多个位置字段与查询匹配也是如此(2.6版后已弃用)\n\n\n\n只能使用$geoNear作为管道的第一阶段。\n必须包含 distanceField 字段。\n$geoNear 需要地理空间索引,如果集合中有多个地理空间索引,请使用 keys参数指定要在计算中使用的字段。如果您只有一个地理空间索引,则$geoNear隐式使用索引字段进行计算。\n不能在 $geoNear 阶段的查询字段中指定 $near 谓词\n视图不支持 geoNear 操作\n从 4.2 版开始,$geoNear 不再有 100 个文档的默认限制\n\n\n示例 1:\n\n查找距离 [ -73.99279 , 40.719296 ] 点最小距离 2m ,并且 category 是 Parks 的所有文档\ndb.citys.aggregate([ { $geoNear: { near: { type: "Point", coordinates: [116.408, 39.904] }, distanceMultiplier: 1/1000, # 计算出的距离按公里显示 distanceField: "dist.calculated", includeLocs: "dist.location", minDistance: 2, spherical: true } }])##### 结果 #####// 1{ "_id": ObjectId("610b931fd2620000ac000144"), "name": "tianjin", "location": { "type": "Point", "coordinates": [ 117.246, 39.117 ] }, "dist": { "calculated": 113.378060984206, # 计算距离的字段 "location": { # 计算中使用的位置的字段 "type": "Point", "coordinates": [ 117.246, 39.117 ] } }}\n\n\n示例2:一个集合有多个地理空间索引\n\nplaces 集合中 location 字段有一个 2dsphere 索引, legacy字段有一个 2d 索引 如下:\n{ "_id" : 3, "name" : "Polo Grounds", "location": { "type" : "Point", "coordinates" : [ -73.9375, 40.8303 ] }, "legacy" : [ -73.9375, 40.8303 ], "category" : "Stadiums"}\n\n使用 key 选项来指定聚集应该使用 location 的字段值的 $geoNear 操作,而不是 legacy 字段值\ndb.places.aggregate([ { $geoNear: { near: { type: "Point", coordinates: [ -73.98142 , 40.71782 ] }, key: "location", distanceField: "dist.calculated", query: { "category": "Parks" } } }, { $limit: 5 }])##### 结果 #####{ "_id" : 8, "name" : "Sara D. Roosevelt Park", "location" : { "type" : "Point", "coordinates" : [ -73.9928, 40.7193 ] }, "category" : "Parks", "dist" : { "calculated" : 974.175764916902 }}{ "_id" : 1, "name" : "Central Park", "location" : { "type" : "Point", "coordinates" : [ -73.97, 40.77 ] }, "legacy" : [ -73.97, 40.77 ], "category" : "Parks", "dist" : { "calculated" : 5887.92792958097 }}\n\n案例# 创建集合db.provinces.insertMany([ { name: "北京", location: { type: "Point", coordinates: [116.41667, 39.91667] } }, { name: "山东", location: { type: "Point", coordinates: [117.000923, 36.675807] }}, { name: "河北", location: { type: "Point", coordinates: [115.48333, 38.03333] } }, { name: "吉林", location: { type: "Point", coordinates: [127.63333, 47.75000] } }, { name: "辽宁", location: { type: "Point", coordinates: [123.38333, 41.80000] } }, { name: "新疆", location: { type: "Point", coordinates: [87.68333, 43.76667] } }, { name: "广东", location: { type: "Point", coordinates: [113.23333, 23.16667] } }, { name: "江西", location: { type: "Point", coordinates: [115.90000, 28.68333] } }, { name: "海南", location: { type: "Point", coordinates: [110.35000, 20.01667] } }, { name: "上海", location: { type: "Point", coordinates: [121.55333, 31.20000] } }, { name: "重庆", location: { type: "Point", coordinates: [106.45000, 29.56667] } }, { name: "天津", location: { type: "Point", coordinates: [117.20000, 39.13333] } },])# 创建地理空间索引db.provinces.createIndex({location: "2dsphere"})# [120.39629, 36.30744]db.provinces.findOne({ geometry: { $geoIntersects: { $geometry: { type: "Point", coordinates: [117.000923, 36.675807] } } }})\n","categories":["MongoDB"],"tags":["MongoDB"]},{"title":"MySQL 索引失效情况","url":"/2021/08/41e3cb957215/","content":"User 表:\n\n\n1:查询的数量是大表的大部分,30%以上,索引失效\n\n\n2:like 以 % 开头,索引失效\n\n################ 创建 name 字段的索引 ################mysql> CREATE INDEX name ON users(name);Query OK, 0 rows affected (0.00 sec)Records: 0 Duplicates: 0 Warnings: 0################ 以 % 开头,索引失效 ################mysql> explain select * from users where name like "%四";+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+| 1 | SIMPLE | users | NULL | ALL | NULL | NULL | NULL | NULL | 5 | 20.00 | Using where |+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+1 row in set, 1 warning (0.00 sec)################ 以 % 结尾,索引生效 ################mysql> explain select * from users where name like "四%";+----+-------------+-------+------------+-------+---------------+------+---------+------+------+----------+-----------------------+| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |+----+-------------+-------+------------+-------+---------------+------+---------+------+------+----------+-----------------------+| 1 | SIMPLE | users | NULL | range | name | name | 1022 | NULL | 1 | 100.00 | Using index condition |+----+-------------+-------+------------+-------+---------------+------+---------+------+------+----------+-----------------------+1 row in set, 1 warning (0.00 sec)\n\n\n3:or 只要其中一个条件没有索引,索引失效\n\n################ 当前 email 字段没有索引 ################mysql> explain select * from users where email = "[email protected]" or name = "赵四";+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+| 1 | SIMPLE | users | NULL | ALL | name | NULL | NULL | NULL | 5 | 36.00 | Using where |+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+1 row in set, 1 warning (0.00 sec) ################ 添加 email 索引 ################mysql> create index email on users(email);Query OK, 0 rows affected (0.02 sec)Records: 0 Duplicates: 0 Warnings: 0################ 索引生效! ################mysql> mysql> explain select * from users where email = "[email protected]" or name = "赵四";+----+-------------+-------+------------+-------------+---------------+------------+-----------+------+------+----------+--------------------------------------+| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |+----+-------------+-------+------------+-------------+---------------+------------+-----------+------+------+----------+--------------------------------------+| 1 | SIMPLE | users | NULL | index_merge | name,email | email,name | 1023,1023 | NULL | 2 | 100.00 | Using union(email,name); Using where |+----+-------------+-------+------------+-------------+---------------+------------+-----------+------+------+----------+--------------------------------------+1 row in set, 1 warning (0.00 sec)\n\n\n4:对于多列索引,不是使用的第一部分(第一个),索引失效\n\nmysql> drop index name on users;Query OK, 0 rows affected (0.00 sec)Records: 0 Duplicates: 0 Warnings: 0mysql> drop index email on users;Query OK, 0 rows affected (0.00 sec)Records: 0 Duplicates: 0 Warnings: 0################ 创建组合索引 ################mysql> create index name_email_age on users(name, email,age);Query OK, 0 rows affected (0.01 sec)Records: 0 Duplicates: 0 Warnings: 0################ 索引生效! ################mysql> explain select * from users where name = '赵四'and age = 51;+----+-------------+-------+------------+------+----------------+----------------+---------+-------+------+----------+-----------------------+| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |+----+-------------+-------+------------+------+----------------+----------------+---------+-------+------+----------+-----------------------+| 1 | SIMPLE | users | NULL | ref | name_email_age | name_email_age | 1023 | const | 1 | 10.00 | Using index condition |+----+-------------+-------+------------+------+----------------+----------------+---------+-------+------+----------+-----------------------+1 row in set, 1 warning (0.00 sec)################ 索引失效! ################mysql> explain select * from users where email = '[email protected]' and age = 51;+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+| 1 | SIMPLE | users | NULL | ALL | NULL | NULL | NULL | NULL | 14 | 7.14 | Using where |+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+1 row in set, 1 warning (0.00 sec)\n\n\n5:数据类型出现隐式转化。如varchar不加单引号的话可能会自动转换为int型,索引失效\n\nmysql> explain select * from users where name = 111;+----+-------------+-------+------------+------+----------------+------+---------+------+------+----------+-------------+| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |+----+-------------+-------+------------+------+----------------+------+---------+------+------+----------+-------------+| 1 | SIMPLE | users | NULL | ALL | name_email_age | NULL | NULL | NULL | 14 | 10.00 | Using where |+----+-------------+-------+------------+------+----------------+------+---------+------+------+----------+-------------+1 row in set, 3 warnings (0.00 sec)mysql> explain select * from users where name = "111";+----+-------------+-------+------------+------+----------------+----------------+---------+-------+------+----------+-------+| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |+----+-------------+-------+------------+------+----------------+----------------+---------+-------+------+----------+-------+| 1 | SIMPLE | users | NULL | ref | name_email_age | name_email_age | 1023 | const | 1 | 100.00 | NULL |+----+-------------+-------+------------+------+----------------+----------------+---------+-------+------+----------+-------+1 row in set, 1 warning (0.00 sec)\n\n\n6:当全表扫描速度比索引速度快时,mysql会使用全表扫描,此时索引失效\n\n\n7:索引列进行运算.需要建立函数索引\n\n","categories":["MySQL"],"tags":["mysql索引"]},{"title":"Sql 语句编写规范","url":"/2021/01/cbc8329bad3b/","content":"1.使用 limit 对查询结果的记录进行限定2. 避免 select * ,将需要查找的字段列出来3. 使用连接(join)来代替子查询4. 拆分大的 delete 或 insert 语句5. 可通过开启慢查询日志来找出较慢的 SQL6. 不做列运算:SELECT id WHERE age + 1 = 10,任何对列的操作都将导致表扫描,它包括数据库教程函数、计算表达式等等,查询时要尽可能将操作移至等号右边7.sql 语句尽可能简单:一条 sql 只能在一个 cpu 运算;大语句拆小语句,减少锁时间;一条大 sql 可以堵死整个库9. 不用函数和触发器,在应用程序实现10. 避免 % xxx 式查询12. 使用同类型进行比较,比如用’123’和’123’比,123 和 123 比15. 列表数据不要拿全表,要使用 LIMIT 来分页,. 每页数量也不要太大\n"},{"title":"Nginx 反向代理","url":"/2021/05/8f50415f2752/","content":"正向代理这里先说下什么是代理(正向代理),一个点外卖的例子,用户通过外卖平台在商家点菜下单的场景,这里的外卖平台就是属于一个代理。还有在使用谷歌搜素的时候不得不科学一下,这里的科学就是一个代理;还比如吃鸡时用的网路加速器。正向代理 时客户端发送对目标服务器的请求,代理服务器在中间将请求转发给目标服务器,并将结果返回给客户端。\n场景:\n\n访问原来无法访问的资源\n加速访问资源\n对客户端访问授权,上网进行认证\n代理可以记录用户访问记录(上网行为管理)\n隐藏客户端身份\n\n反向代理由于单机服务器处理请求的能力有限,处理大量客户端请求时压力很大,甚至出现问题,所以在增加目标服务器的同时,在这些 目标服务器和客户端之间增加代理服务器负责接收客户端请求,并分发给目标服务器,然后在把目标服务器的响应结果返回给客户端。类似于拨打客服热线时,热线电话是固定的,但是会分配不同客服人员接听来解决问题。\n场景:\n\n保护和隐藏原始资源服务器,防止攻击,通常将反向代理作为公网访问地址,Web服务器是内网\n负载均衡,通过反向代理来进行服务器资源的优化处理\n加密和 SSL 加速\n缓存静态内容\n压缩、减速上传、安全、外网发布等\n\n\n这两者的主要区别就是 正向代理的代理对象是客户端,反向代理的代理对象是服务端。\n\nNginx 反向代理的实现要实现 Nginx 反向代理,这里就要提到它的 http_proxy_module 模块,它允许将请求传递给另一个服务器。\n测试:\n简单使用 NodeJS 编写一个 HTTP 服务器:\nconst http = require('http');const server = http.createServer(function (req, res) { if (req.url == '/') { res.end('Hello world'); }}).listen('888', 'localhost');\n访问 http://localhost:888\nHTTP/1.1 200 OKDate: Wed, 29 Dec 2021 07:32:49 GMTConnection: keep-aliveContent-Length: 11\n\nNginx 配置代理:\nserver { listen 80; server_name my_proxy.test location / { proxy_pass http://localhost:888; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }}\n\n访问 http://my_proxy.test\nHTTP/1.1 200 OKserver: nginx/1.11.5date: Wed, 29 Dec 2021 07:32:22 GMTtransfer-encoding: chunkedconnection: keep-alive\n\n常用配置项\nproxy_pass\n 监听代理服务器的端口或套接字,以及将被反射到的URI的位置。端口可以用主机名或地址和端口的名称来表示\n proxy_pass http://localhost:8000/uri/;// 或者proxy_pass http://unix:/tmp/backend.socket:/uri/;\nproxy_set_header\n重定义或添加一些请求头行,这些请求头行将被传输到代理服务器。作为值,可以使用文本、变量及其组合。\n\nproxy_read_timeout\n定义从代理服务器读取响应的超时。超时时间只设置在两个连续的读操作之间,而不是整个响应的传输。如果代理服务器在此时间内没有传输任何内容,则连接将被关闭。默认 60s\n\nproxy_send_timeout\n设置将请求传输到代理服务器的超时时间。超时时间只设置在两个连续的写操作之间,而不是整个请求的传输。如果代理服务器在此时间内没有接收到任何信息,则连接将被关闭。默认 60s。\n\nproxy_connect_timeout\n定义与代理服务器建立连接的超时时间。应该注意的是,这个超时通常不能超过75秒。默认 60s。\n\nproxy_http_version\n设置代理使用的 HTTP 协议版本。默认情况下,使用 1.0版本。对于 keepalive 连接和 NTLM 身份验证,建议使用 1.1版本。\n\n\n详细配置见:http://nginx.org/en/docs/http/ngx_http_proxy_module.html\n","categories":["Nginx"],"tags":["nginx","反向代理","proxy","http_proxy_module"]},{"title":"PHP 中 Redis 常用操作(list 类型)","url":"/2021/07/bd70d53460ec/","content":"Redis列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)\n一个列表最多可以包含232 - 1 个元素 (4294967295, 每个列表超过40亿个元素)。\n将元素压入链表(lPush)\nlPush() 方法将数据从左侧压入列表\nrPush() 方法将数据从右侧压入列表\n$redis->lPush('list1', 'v1', 'v2', 'v3', 'v4')$redis->lPush('list2', 'v1', 'v2', 'v3', 'v4')var_dump( $redis->lRange('list1', 0, -1) );var_dump( $redis->lRange('list2', 0, -1) );// list1:// array(4) {// [0]=> string(2) "v4"// [1]=> string(2) "v3"// [2]=> string(2) "v2"// [3]=> string(2) "v1"// }// list2:// array(4) {// [0]=> string(2) "v1"// [1]=> string(2) "v2"// [2]=> string(2) "v3"// [3]=> string(2) "v4"// }\n\n在某个位置插入新元素(lInsert)\nRedis::BEFORE 在之前插入\nRedis::AFTER 在之后插入\n$redis->lPush('list1', 'A', 'B', 'C', 'v4')$redis->lInsert('list', Redis::BEFORE, 'C', 'X');$redis->lInsert('list', Redis::AFTER, 'C', 'Y');$redis->lRange('list', 0, -1);// array('A', 'B', 'X', 'C','Y')\n\n设置、获取某个元素的值(lSet、lGet)\nlSet() 方法可以通过下标修改链表元素的值,下标是从0开始lGet() 方法可以通过下标获取链表元素的值,下标是从0开始\n$redis->lPush('list1', 'A', 'B', 'C')$redis->lGet('key1', 0); // 'A'$redis->lSet('key1', 0, 'X');$redis->lGet('key1', 0); // 'X'\n\n获取列表元素个数(lLen)\n$length = $redis->lLen('list');echo $length;\n\n获取下标对应的元素(lIndex)\n$val = $redis->lIndex('list', 1);echo $val;\n\n获取某个选定范围元素集(lRange)\n通过起止下标来获取列表某个范围内的元素集\n$arr = $redis->lRange('list', 0, 1); //前两个元素$arr = $redis->lRange('list', 0, -1); //全部元素$arr = $redis->lRange('list', -2, -1); //后两个元素var_dump($arr);\n\n从列表左侧弹出数据(lPop)\nlPop() 方法将数据从列表左侧弹出,返回弹出的元素,数据元素在list中消失。\nrPop() 右侧弹出\n$redis->lPush('list', 'A', 'B', 'C')$val = $redis->lPop('list'); // list => [ 'B', 'C' ]$val = $redis->rPop('list'); // list => [ 'B' ]\n\n根据值移除元素(lRem)\n根据值来移除元素,并且可以指定要移除的元素个数,因为 list 中可能出现重复的元素\n$redis->lPush('list', 'A', 'A', 'C', 'B', 'A')$redis->lRange('key1', 0, -1); // array('A', 'A', 'C', 'B', 'A')$redis->lRem('key1', 'A', 2);$redis->lRange('key1', 0, -1); // array('C', 'B', 'A')\n\n","categories":["Redis"],"tags":["php","redis"]},{"title":"PHP 中 Redis 常用操作(set 类型)","url":"/2021/07/ac3227b76e78/","content":"Redis set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的接口,这是也是list所不能提供了。\nRedis的Set是string类型的无需集合。它底层其实是一个value为null的hash表,所以添加、删除、查找的复杂度都是O(1)。\n集合数据的特征:\n\n元素不能重复,保持唯一性\n元素无序,不能使用索引(下标)操作\n\n添加元素到集合(sAdd)\n$redis->sAdd('k', 'v1'); // int(1)$redis->sAdd('k', 'v1', 'v2', 'v3'); // int(2)\n\n随机获取一个元素或多个(sPop)\n无序性,是随机的\n$redis->sAdd('k', 'v1', 'v2', 'v3', 'v4');$redis->sPop('k'); // 'v1',$redis->sPop('k'); // 'v3',$redis->sPop('k', 2); // ['v1','v3'],\n\n删除集合里指定的值(sRem)\n$redis->sAdd('k', 'v1', 'v2', 'v3');$redis->sRem('k', 'v2', 'v3'); // k => array('v1')\n\n遍历集合(sScan)\n//不使用迭代器,匹配所有的元素,进行遍历$iterator = null;$elements = $redis->sScan('team', $iterator, '*');foreach ($elements as $element) { echo $element, '<br>';}\n\n获取所有成员(sMembers)\n$redis->sAdd('k', 'v1', 'v2', 'v3');$members = $redis->sMembers('k');var_dump($members);\n\n获取集合元素个数(sCard)\n$redis->sAdd('k', 'v1', 'v2', 'v3');$redis->sCard('k'); // 3\n\n并集(sUnion),差集(sDiff),交集(sInter)\n$redis->sAdd('s0', '1', '2');$redis->sAdd('s1', '3', '1');$redis->sAdd('s2', '3', '4');var_dump($redis->sUnion('s0', 's1', 's2'));//array(4) {// [0]=>// string(1) "3"// [1]=>// string(1) "4"// [2]=>// string(1) "1"// [3]=>// string(1) "2"//}var_dump($redis->sDiff('s1', 's2'));//array(2) {// [0]=>// string(1) "1"// [0]=>// string(1) "4"// }var_dump($redis->sDiff('s1', 's2'));//array(1) {// [0]=>// string(1) "3"// }\n\n","categories":["Redis"],"tags":["php","redis"]},{"title":"PHP 中 Redis 常用操作(string 类型)","url":"/2021/07/634c5e00d421/","content":"\nstring 是 Redis 最基本的类型,和 Memcached 一模一样的类型,一个key对应一个value。\n string 类型是二进制安全的。这意味着 Redis 的 string 可以包含任何数据。比如JPG图片或者序列化的对象。\n 一个 Redis 中字符串 value 最多可以是512M\n\n设置(set)\n仅支持字符串操作,不支持内置数据编码功能。如果需要存储PHP的非字符串类型,需要提前手动序列化,获取时再反序列化。\n$user = [ 'name' => 'zhaosi', 'age' => 49,];//将$user数组序列化成json字符串$user = json_encode($user);$redis->set('user', $user);$data = $redis->get('user');//拿到序列化后的字符串,再反序列化成PHP数组$data = json_decode($data, true);var_dump($data);\nsetnx() 方法是只有在 key 不存在时设置 key 的值,\n设置并指定过期时间(setex)\n设置键的同时,设置过期时间(秒)\n$redis->setex('user', 60, 'user_1256465');\n\n获取值(get)\n$redis->set('name', 'liuneng'); //设置$name = $redis->get('name'); //获取var_dump($name);\n\n增加(incr, incrBy)\nincr()、incrBy() 都是操作数字,对数字进行增加的操作,incr是执行原子加1操作,incrBy是增加指定的数\n$redis->set('age', 49);$redis->incr('age'); //等于$age++$redis->incrBy('age', 10); // 等于$age = $age + 10\n\n减少(decr, decrBy)\ndecr() 和 decrBy() 方法是对数字进行减的操作,和 incr 正好相反\n$redis->set('age', 20);$redis->decr('age'); //等于$age--$redis->decrBy('age', 10); // 等于$age = $age - 5\n\n追加(append)\nappend() 表示往字符串后面追加元素,返回值是字符串的总长度\n示例:在’hello’后面追加’ world’\n$redis->set('welcome', 'hello');$length = $redis->append('welcome', ' world');var_dump($length);\n\n获取长度(strLen)\nstrLen() 方法可以获取字符串的长度\n$redis->set('name', 'zhaosi');$length = $redis->strlen('name');var_dump($length); // 6\n\n字符串截取(getRange)\ngetRange() 方法可以用来截取字符串的部分内容,第二个参数是下标索引的开始位置,第三个参数是下标索引的结束位置(不是要截取的长度),\n$redis->set('ID', '411521199809151234');$subStr = $redis->getRange('ID', 0, 5);var_dump($subStr);\n\n返回所有指定键的值mget()\n$redis->set('user_1','zhaosan');$redis->set('user_2','zhaosi');$data = $redis->mget(['user_1','user_2']);var_dump($data);\n\n设置多个 key-value mset()\n$redis->mset(array('key0' => 'value0', 'key1' => 'value1'));var_dump($redis->get('key0'));var_dump($redis->get('key1'));\n设置一个值并返回该键处的前一个条目\n$redis->set('x', '42');$exValue = $redis->getSet('x', 'lol'); // return '42', replaces x by 'lol'$newValue = $redis->get('x')' // return 'lol'\n…..\n","categories":["Redis"],"tags":["php","redis"]},{"title":"PHP 中 Redis 常用操作(zset 类型)","url":"/2021/07/c6af5ed70b30/","content":"Redis 有序集合和集合一样也是string类型元素的集合,且不允许重复的成员。\n不同的是每个元素都会关联一个 double 类型的分数。redis 正是通过分数来为集合中的成员进行从小到大的排序。\n有序集合的成员是唯一的,但分数(score)却可以重复。处理元素时,也要加上score的处理\n集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。 集合中最大的成员数为 232 - 1 (4294967295, 每个集合可存储40多亿个成员)。\n添加元素(zAdd)\n$redis->zAdd('zSet', 10, 'A');$redis->zAdd('zSet', 106, 'B');$redis->zAdd('zSet', 20, 'C');$redis->zAdd('zSet', 15, 'D');$redis->zAdd('zSet', 11, 'E');\n\n元素分值增减(zIncrBy)\n$redis->zIncrBy('zSet', 20.5, 'A'); // 30.5$redis->zIncrBy('zSet', 12, 'B'); // 128\n\n获取根据 score 排序后的数据段(zRange,zRevRange)\n// 升序$redis->zRange('zSet', 0, -1)//array (size=5)// 0 => string 'E' (length=1)// 1 => string 'D' (length=1)// 2 => string 'C' (length=1)// 3 => string 'A' (length=1)// 4 => string 'B' (length=1)// 降序// 分值前三$redis->zRevRange('zSet', 0, 2, true);//array (size=3)// 'B' => float 118// 'A' => float 30.5// 'C' => float 20\n\n获取 score 过滤后排序的数据段(zRangeByScore,zRevRangeByScore)\n根据分值过滤之后的列表\n需要提供分值区间\n$redis->zAdd('key', 0, 'val0');$redis->zAdd('key', 2, 'val2');$redis->zAdd('key', 10, 'val10');$redis->zRangeByScore('key', 0, 3); // array('val0', 'val2')$redis->zRangeByScore('key', 0, 3, array('withscores' => TRUE); // array('val0' => 0, 'val2' => 2)$redis->zRangeByScore('key', 0, 3, array('limit' => array(1, 1)); // array('val2' => 2)$redis->zRangeByScore('key', 0, 3, array('limit' => array(1, 1)); // array('val2')$redis->zRangeByScore('key', 0, 3, array('withscores' => TRUE, 'limit' => array(1, 1)); // array('val2' => 2)\n\n获取元素个数(zCard)\n$redis->zCard('zSet'); // \n\n获取区间内的元素个数(zCount)\n$redis->zAdd('key', 0, 'val0');$redis->zAdd('key', 2, 'val2');$redis->zAdd('key', 10, 'val10');// 0 ~ 3 之间$redis->zCount('key', 0, 3); // array('val0', 'val2')\n\n获取元素的 score (zScore)\n$redis->zScore('zSet', 'A') // 30.5\n\n获取某个元素在集合中的排名(zRank)\n从0开始\n$redis->zRank('zSet', 'A'); // 3\n\n删除元素(zRem)\n$redis->zRem('zSet', 'A');\n\n根据排名来删除(zRemRangeByRank)\n//按照升序排序删除第一个和第二个元素$redis->zRemRangeByRank('zSet', 0, 1)\n\n根据区间来删除(zRemRangeByScore)\n//删除 score 在[15, 30]之间的元素$redis->zRemRangeByScore('zSet', 15, 30);\n\n","categories":["Redis"],"tags":["php","redis"]},{"title":"PHP 中 Redis 常用操作(基础操作)","url":"/2021/07/6042abf4cf50/","content":"连接 redis 服务器,实例化 redis 对象\n$redis = new Redis();if (!$redis->connect('127.0.0.1', 6379)) { trigger_error('Redis连接出错!!!', E_USER_ERROR);} else { echo "连接正常 \\n";}\n获取所有的key(keys)\n$data = $redis->keys('*');var_dump($data);\n判断键对应值的类型(type)\ntype() 方法用户获取一个key对应值的类型,返回值(1:string, 2:set, 3:list, 4:zset, 5:hash 6:未知)\n删除缓存项(del)\n$redis->del('用删除的key')\n设置有效期(expire,expireAt)\nexpire() 设置某个时间段后过期\nexpireAt() 在某个时间点(时间戳)过期失效\n$redis->set('user', 'user_123456');$redis->expire('user', 3600); // 3600 秒后过期$redis->expireAt('user', strtotime('2020-08-15 00:00:00'));\n\n获取有效期(ttl)\nttl() 获取某个键的剩余有效期\n$redis->ttl('key'); //获取剩余有效期,单位:秒(s)$redis->pttl('key'); //获取剩余有效期,单位:毫秒(ms) \n\n检测缓存项是否存在(exists)\nexists() 方法用于检测某个key是否存在\n$redis->set('age', 25);if ($redis->exists('age')) { echo '存在';} else { echo '不存在';}\n\n查看当前数据库key的数量(dbSize)\n$dbSize = $redis->dbSize();echo $dbSize;\n\n清空当前数据库(flushDB)\n$isFlushed = $redis->flushDB();var_dump($isFlushed);\n\n清空所有数据库(flushAll)\n会清空所有库的数据,默认是0~15这16个数据库\n$isFlushed = $redis->flushAll();var_dump($isFlushed);","categories":["Redis"]},{"title":"PHP 中 Redis 常用操作(hash 类型)","url":"/2021/07/124fc284dddc/","content":"Redis hash 是一个 string 类型的 field(字段) 和 value(值) 的映射表,hash 特别适合用于存储对象。\nRedis 中每个 hash 可以存储 232 - 1 键值对(40多亿)。\n当前服务器一般都是将用户登录信息保存到Redis中,这里存储用户登录信息就比较适合用hash表。hash表比string更合适,如果我们选择使用string类型来存储用户的信息的时候,我们每次存储的时候就得先序列化(json_encode()、serialize())成字符串才能存储redis,\n从redis拿到用户信息后又得反序列化(json_decode()、unserialize())成数组或对象,这样开销比较大。如果使用hash的话我们通过key(用户ID)+field(属性标签)就可以操作对应属性数据了,既不需要重复存储数据,也不会带来序列化和并发修改控制的问题。\n设置(hSet)\n$redis->hSet('user', 'name', 'zhaosi');$redis->hSet('user', 'age', 50);$redis->hSet('user', 'address', '象牙山');\n\n批量设置(hMset)\n$u1 = [ 'id'=> 1, 'name' => 'liuneng', 'age' => 51, 'address' => '象牙山'];$redis->hMSet('user:'.$u1['id'], $u1);$u2 = [ 'id' => 2, 'name' => 'zhaosi', 'age' => 50, 'address' => '象牙山'];$redis->hMSet('user:'.$u2['id'], $u2);\n为什么要给存储的数据加一个前缀呢,比如说上面示例的user前缀?\n因为我们在 redis 一般需要存储很多业务类型的数据,比如用户登录信息、验证码信息,我们都是以用户唯一标识信息(如id,手机号)作为 key,如果不加前缀就会导致多个业务类型的数据就存到一起了,这是不合理也不应该的。所以我们可以以业务名称作为前缀然后配合上用户唯一标识即前缀:唯一标识作为 key,中间是用冒号 : 分隔,这样就可以把数据按照业务类型分开,这也是业界通用的做法, php 中 session 存储默认也是一 PHPREDIS_SESSION 作为前缀,官方都是这么做了,我们还有什么理由不这样做呢。\n获取(hGet)\n$redis->hGet('user', 'name'); // zhaosi\n\n获取全部元素(hGetAll)\n$redis->hGetAll('user');//[// 'name' => 'zhaosi',// 'age' => 50,// 'address' => '象牙山'//]\n\n删除某个元素(hDle)\n$redis->hDel('user', 'address');\n\n判断元素是否存在(hExists)\n$redis->hExists('user', 'address'); // false\n\n获取长度(hLen)\n$redis->hSet('user', 'name', 'zhaosi');$redis->hSet('user', 'age', 50);$redis->hSet('user', 'address', '象牙山');$redis->hLen('user'); // 3\n\n","categories":["Redis"],"tags":["php","redis"]},{"title":"php、mysql、redis 慢日志 slowlog","url":"/2021/05/9ad1242d5530/","content":"MySQl 慢日志查看是否开启慢查询日志功能:\nmysql> show variables like 'slow_query%';+---------------------+-----------------------------------+| Variable_name | Value |+---------------------+-----------------------------------+| slow_query_log | OFF || slow_query_log_file | /var/lib/mysql/homestead-slow.log |+---------------------+-----------------------------------+2 rows in set (0.01 sec)mysql> show variables like 'long_query_time';+-----------------+-----------+| Variable_name | Value |+-----------------+-----------+| long_query_time | 10.000000 |+-----------------+-----------+1 row in set (0.03 sec)\n\n\nslow_query_log:开启状态(on/off)\nslow_query_log_file:慢查询日志存放文件位置\nlong_query_time:超过多长时间会记录\n\n临时配置\n# 开启慢日志mysql> set global slow_query_log='ON';Query OK, 0 rows affected (0.01 sec)# 指定存放路径mysql> set global slow_query_log_file='/var/lib/mysql/slow.log' -> ;Query OK, 0 rows affected (0.02 sec)# 配置超出时间mysql> set global long_query_time=2;Query OK, 0 rows affected (0.00 sec)mysql> show variables like 'slow_query%';+---------------------+-------------------------+| Variable_name | Value |+---------------------+-------------------------+| slow_query_log | ON || slow_query_log_file | /var/lib/mysql/slow.log |+---------------------+-------------------------+2 rows in set (0.00 sec)\n\n永久配置\n修改 mysql 配置文件\n# Here you can see queries with especially long durationslow_query_log = 1slow_query_log_file = /var/log/mysql/mysql-slow.loglong_query_time = 2\n重启 mysql 完成。\nRedis 慢日志临时配置(当前会话)\n127.0.0.1:6379> config set slowlog-log-slower-than 10000OK127.0.0.1:6379> config get slowlog-log-slower-than1) "slowlog-log-slower-than"2) "10000"\n\n永久配置\n修改配置文件\n################################## SLOW LOG #################################### The following time is expressed in microseconds, so 1000000 is equivalent# to one second. Note that a negative number disables the slow log, while# a value of zero forces the logging of every command.slowlog-log-slower-than 10000# There is no limit to this length. Just be aware that it will consume memory.# You can reclaim memory used by the slow log with SLOWLOG RESET.slowlog-max-len 128\n\n查看慢查询日志\n127.0.0.1:6379> slowlog get1) 1) (integer) 4 # 唯一标识符 2) (integer) 1638858424 # 执行时系统时间戳 3) (integer) 5 # 执行时长(微妙) 4) 1) "get" # 命令 2) "name" 5) "127.0.0.1:46102" 6) ""\n\nPHP 慢日志在配置文件 php-fpm.conf 或者 www.conf, 查找 slowlog 。\n; The log file for slow requests; Default Value: not set; Note: slowlog is mandatory if request_slowlog_timeout is setslowlog = log/$pool.log.slow; The timeout for serving a single request after which a PHP backtrace will be; dumped to the 'slowlog' file. A value of '0s' means 'off'.; Available units: s(econds)(default), m(inutes), h(ours), or d(ays); Default Value: 0request_slowlog_timeout = 1; Depth of slow log stack trace.; Default Value: 20request_slowlog_trace_depth = 20\n设置的路径需要存在的路径,或提前手动创建\n查看\n[07-Dec-2021 07:16:41] [pool www] pid 6249script_filename = /home/vagrant/code/MeEdu/public/index.php[0x00007f0c4c61c420] Composer\\Autoload\\includeFile() /home/vagrant/code/MeEdu/vendor/composer/ClassLoader.php:322[0x00007f0c4c61c380] loadClass() unknown:0[0x00007f0c4c61c320] spl_autoload_call() unknown:0[0x00007ffe3c6eeb40] ???() /home/vagrant/code/MeEdu/public/index.php:55\n\n\n\n\n\n\n\n\n\n\n\n\n\n","categories":["运维"],"tags":["php","redis","mysql","slowlog"]},{"title":"使用 WSL 安装环境时的问题记录","url":"/2021/12/27dbc8e7aedb/","content":"\nWLS 安装文档:https://docs.microsoft.com/zh-cn/windows/wsl/install\n\n在 Microsoft Store 下载 Ubuntu 时 Store 打不开空白页或者是提示没有网络\n\n检测网络是否配置了代理,关闭\n设置:网络——属性——Internet 选项——连接——局域网设置——自动检测设置\n\n安装 Ubuntu 后打开报错 0x80080005Installing, this may take a few minutes...WslRegisterDistribution failed with error: 0x80080005Error: 0x80080005 ???????Press any key to continue...\n\n管理员身份打开命令提示符,尝试重启 Linux 子系统的管理器:\nsc stop LxssManager\nsc start LxssManager\n然后查询确保状态是4,Running.\nsc query LxssManager\n再次尝试打开 Ubuntu,如果这次再次出现不同的错误,尝试卸载并重新安装该应用程序\n修改 WLS 安装目录在 Microsoft Store 安装的 Ubuntu 默认安装到了 C盘,下面把它修改到 D盘。\n运行管理员命令窗\n查看所有分发版本:\nC:\\WINDOWS\\system32>wsl -l -v NAME STATE VERSION* docker-desktop-data Running 2 docker-desktop Running 2 Ubuntu-20.04 Running 2\n导出分发版为 TAR 文件到 D盘:\nwsl --export Ubuntu-20.04 d:\\ubuntu20.04.tar\n注销当前分发版:\nwsl --unregister Ubuntu-20.04\n导入并安装:\nwsl --import Ubuntu-20.04 d:\\Ubuntu20.04 d:\\ubuntu20.04.tar --version 2\n设置默认登陆用户为安装时用户名:\nubuntu2004 config --default-user Username\n\n\n","categories":["其他"],"tags":["笔记","WSL"]},{"title":"发布的 composer 包安装报错 Could not find package xxxx/xxxx","url":"/2022/02/245589d0be12/","content":"报错一:\n[InvalidArgumentException]Could not find a version of package suzhif/laravel-workerman matching your minimum-stability (dev). Require it with an explicit version constraint allowing its desiredstability.\n\n原因: 新发布的 composer 包没有设置版本号\ncomposer require xxxxx/xxxxx:dev-master\n\n报错二:\n[InvalidArgumentException]Could not find package xxxxx/xxxxx.\nDid you mean this?xxxxx/xxxxx\n\ncomposer -v 查看版本号。composer 1.* 版本已经不在支持新发布的包,升级到 2.* 版本。\ncomposer self-update\n\n升级后再安装\n","categories":["其他"],"tags":["笔记","composer"]},{"title":"RabbitMQ - Topic 模式","url":"/2022/01/3619d7c118f9/","content":"\n官网教程:https://www.rabbitmq.com/tutorials/tutorial-five-php.html\n\n\n\nMessages sent to a topic exchange can’t have an arbitrary routing_key - it must be a list of words, delimited by dots. The words can be anything, but usually they specify some features connected to the message. A few valid routing key examples: “stock.usd.nyse”, “nyse.vmw”, “quick.orange.rabbit”. There can be as many words in the routing key as you like, up to the limit of 255 bytes.发送到主题交换的消息不能有任意的 routing_key —— 它必须是由点分隔的单词列表。这些词可以是任何词,但通常它们指明了与信息相关的一些特征。以下是一些有效的路由关键示例:"stock.usd.nyse"、"nyse.vmw"、"quick.orange.rabbit"。路由键中可以有任意多的单词,最多为 255 个字节。The binding key must also be in the same form. The logic behind the topic exchange is similar to a direct one - a message sent with a particular routing key will be delivered to all the queues that are bound with a matching binding key.绑定键的形式也必须相同。topic 交换机背后的逻辑与 direct 交换机类似 —— 带有特定路由键的消息将被发送到所有绑定了匹配绑定键的队列。\n\n绑定键有两个重要的特殊情况: - *(星号)可以只代替一个单词。 - #(井号)可以代替零个或多个单词。\n生产者<?phprequire_once __DIR__ . '/vendor/autoload.php';use PhpAmqpLib\\Connection\\AMQPStreamConnection;use PhpAmqpLib\\Message\\AMQPMessage;$connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest');$channel = $connection->channel();$exchange = 'topic_exchange';$channel->exchange_declare($exchange, 'topic', false, false, false);$routing_key = isset($argv[1]) && !empty($argv[1]) ? $argv[1] : 'anonymous.info';$data = implode(' ', array_slice($argv, 2));if(empty($data)) $data = "Hello World!";$msg = new AMQPMessage($data);$channel->basic_publish($msg, $exchange, $routing_key);echo " [x] 发送 ",$routing_key,':',$data," \\n";$channel->close();$connection->close();\n\n消费者<?phprequire_once __DIR__ . '/vendor/autoload.php';use PhpAmqpLib\\Connection\\AMQPStreamConnection;$connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest');$channel = $connection->channel();$exchange = 'topic_exchange';$channel->exchange_declare($exchange, 'topic', false, false, false);list($queue_name, ,) = $channel->queue_declare("", false, false, true, false);$binding_keys = array_slice($argv, 1);if( empty($binding_keys )) { file_put_contents('php://stderr', "Usage: $argv[0] [binding_key]\\n"); exit(1);}foreach($binding_keys as $binding_key) { $channel->queue_bind($queue_name, $exchange, $binding_key);}echo ' [*] Waiting for logs. To exit press CTRL+C', "\\n";$callback = function($msg){ echo ' [x] ',$msg->delivery_info['routing_key'], ':', $msg->body, "\\n";};$channel->basic_consume($queue_name, '', false, true, false, false, $callback);while(count($channel->callbacks)) { $channel->wait();}$channel->close();$connection->close();\n\n运行>php receive_logs_topic.php "#" [*] Waiting for logs. To exit press CTRL+C [x] error.order:Error: xxxxxxxxxxxx [x] order:创建订单 [x] error.user.order:Error:xxx [x] error.user:Error:user xxxxx [x] info.user:INFO:user xxxxxxx\n\n>php receive_logs_topic.php "error.*" [*] Waiting for logs. To exit press CTRL+C [x] error.order:Error: xxxxxxxxxxxx [x] error.user:Error:user xxxxx\n\n>php receive_logs_topic.php "#.user" [*] Waiting for logs. To exit press CTRL+C [x] error.user:Error:user xxxxx [x] info.user:INFO:user xxxxxxx\n\n>php emit_log_topic.php "error.order" "Error: xxxxxxxxxxxx" [x] Sent error.order:Error: xxxxxxxxxxxx>php emit_log_topic.php "order" "创建订单" [x] Sent order:创建订单>php emit_log_topic.php "error.user.order" "Error:xxx" [x] Sent error.user.order:Error:xxx>php emit_log_topic.php "error.user" "Error:user xxxxx" [x] Sent error.user:Error:user xxxxx>php emit_log_topic.php "info.user" "INFO:user xxxxxxx" [x] Sent info.user:INFO:user xxxxxxx","categories":["消息队列"],"tags":["RabbitMQ","消息队列"]},{"title":"RabbitMQ - 工作队列","url":"/2022/01/75aef2175824/","content":"\n官网教程:https://www.rabbitmq.com/tutorials/tutorial-two-php.html\n\n\n轮询调度\n生产者:<?phprequire_once '../../vendor/autoload.php'; use PhpAmqpLib\\Connection\\AMQPStreamConnection;use PhpAmqpLib\\Message\\AMQPMessage;$connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest');$channel = $connection->channel();$queue_name = 'queue_work';$channel->queue_declare($queue_name, false, true, false, false);for ($i = 1; $i <= 20; $i++) { $msg = new AMQPMessage('消息' . $i); $channel->basic_publish($msg, '', $queue_name);}$channel->close();$connection->close();\n消费者: work_1.php<?phprequire_once '../../vendor/autoload.php';use PhpAmqpLib\\Connection\\AMQPStreamConnection;use PhpAmqpLib\\Message\\AMQPMessage;$connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest');$channel = $connection->channel();$queue_name = 'queue_work';$channel->queue_declare($queue_name, false, true, false, false);echo "work1 开始接收消息……\\n";$channel->basic_consume($queue_name, '', false, false, false, false, function (AMQPMessage $msg) { echo '接收到消息:', $msg->body, "\\n"; sleep(1); // 模拟延迟 1s });while (count($channel->callbacks)) { $channel->wait();}$channel->close();$connection->close();\n work_2.php<?phprequire_once '../../vendor/autoload.php';use PhpAmqpLib\\Connection\\AMQPStreamConnection;use PhpAmqpLib\\Message\\AMQPMessage;$connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest');$channel = $connection->channel();$queue_name = 'queue_work';$channel->queue_declare($queue_name, false, true, false, false);echo "work2 开始接收消息……\\n";$channel->basic_consume($queue_name, '', false, false, false, false, function (AMQPMessage $msg) { echo '接收到消息:', $msg->body, "\\n"; });while (count($channel->callbacks)) { $channel->wait();}$channel->close();$connection->close();\n运行>php work_1.phpwork1 开始接收消息……接收到消息:消息1接收到消息:消息3接收到消息:消息5接收到消息:消息7接收到消息:消息9接收到消息:消息11接收到消息:消息13接收到消息:消息15接收到消息:消息17接收到消息:消息19\n>php work_2.phpwork2 开始接收消息……接收到消息:消息2接收到消息:消息4接收到消息:消息6接收到消息:消息8接收到消息:消息10接收到消息:消息12接收到消息:消息14接收到消息:消息16接收到消息:消息18接收到消息:消息20\n\n公平分发\n生产者\n<?phprequire_once '../../vendor/autoload.php';use PhpAmqpLib\\Connection\\AMQPStreamConnection;use PhpAmqpLib\\Message\\AMQPMessage;$connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest');$channel = $connection->channel();$queue_name = 'queue_work2';$channel->queue_declare($queue_name, false, true, false, false);for ($i = 1; $i <= 20; $i++) { $msg = new AMQPMessage('消息' . $i, [ 'delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT // 将消息标记为持久性 ]); $channel->basic_publish($msg, '', $queue_name);}$channel->close();$connection->close();\n消费者 work_1.php\nrequire_once '../../vendor/autoload.php';use PhpAmqpLib\\Connection\\AMQPStreamConnection;use PhpAmqpLib\\Message\\AMQPMessage;$connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest');$channel = $connection->channel();$queue_name = 'queue_work2';$channel->queue_declare($queue_name, false, true, false, false);echo "work1 开始接收消息……\\n";// 限制每次只接收处理一条消息$channel->basic_qos(null, 1, null);$channel->basic_consume($queue_name, '', false, false, false, false, function (AMQPMessage $msg) { echo '接收到消息:', $msg->body, "\\n"; sleep(1); $msg->ack(); // 消息确认 });while (count($channel->callbacks)) { $channel->wait();}\n\n work_2.php\nrequire_once '../../vendor/autoload.php';use PhpAmqpLib\\Connection\\AMQPStreamConnection;use PhpAmqpLib\\Message\\AMQPMessage;$connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest');$channel = $connection->channel();$queue_name = 'queue_work2';$channel->queue_declare($queue_name, false, true, false, false);echo "work2 开始接收消息……\\n";// 限制每次只接收处理一条消息$channel->basic_qos(null, 1, null);$channel->basic_consume($queue_name, '', false, false, false, false, function (AMQPMessage $msg) { echo '接收到消息:', $msg->body, "\\n"; $msg->ack(); // 消息确认 });while (count($channel->callbacks)) { $channel->wait();}\n运行\n> php work_1.phpwork1 开始接收消息……接收到消息:消息2\n> php work_2.phpwork2 开始接收消息……接收到消息:消息1接收到消息:消息3接收到消息:消息4接收到消息:消息5接收到消息:消息6接收到消息:消息7接收到消息:消息8接收到消息:消息9接收到消息:消息10接收到消息:消息11接收到消息:消息12接收到消息:消息13接收到消息:消息14接收到消息:消息15接收到消息:消息16接收到消息:消息17接收到消息:消息18接收到消息:消息19接收到消息:消息20\n\n","categories":["消息队列"],"tags":["RabbitMQ","消息队列"]},{"title":"RabbitMQ - 死信队列","url":"/2022/01/22c89a6e7b63/","content":"\n官网:https://www.rabbitmq.com/dlx.htmlhttps://www.rabbitmq.com/ttl.html\n\nDLX (Dead Letter Exchanges) 当消息在一个队列中变成 Dead 之后,它能被重新发送到另一个交换机中,这个交换机就是 DLX。\n产生原因: - 使用 basic.reject 或 basic.nack 并将 requeue 参数设置为false ,消息被拒绝。 - 消息在队列中停留的时间超过了配置的 TTL - 队列达到最大长度,先入队的消息会被删除\n注意:队列的过期不会死信其中的消息。\n\nDLX 是一个普通的交换机,和一般的交换机没有区别,它能在任何的队列上被指定,实际上就是设置某一个队列的属性。当这个队列中存在死信时,RabbitMQ 就会自动地将这个消息重新发布到设置的 DLX 上去,进而被路由到另一个队列,即死信队列。\n创建一个死信队列<?phprequire_once '../vendor/autoload.php';use PhpAmqpLib\\Connection\\AMQPStreamConnection;use PhpAmqpLib\\Message\\AMQPMessage;use PhpAmqpLib\\Wire\\AMQPTable;$connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest');$channel = $connection->channel();// 创建一个处理死信的交换机和队列$dead_exchange = 'dead_direct_exchange';$dead_queue_name = 'dead_queue';$dead_routing_key = 'dead';$channel->exchange_declare($dead_exchange, 'direct', false, false, false);$channel->queue_declare($dead_queue_name, false, true, false, false);$channel->queue_bind($dead_queue_name, $dead_exchange, $dead_routing_key);$ttl_exchange = 'ttl_exchange';$ttl_routing_key = 'ttl_message';$channel->exchange_declare($ttl_exchange, 'direct', false, false, false);// 创建一个 TTL 消息的队列$arguments = new AMQPTable();$arguments->set('x-message-ttl', 5000); // 设置队列中消息的过期时间 5s$arguments->set('x-dead-letter-exchange', $dead_exchange); // 设置处理死信消息的交换机$arguments->set('x-dead-letter-routing-key', $dead_routing_key); // 指定一个路由关键字用于死信消息。如果没有设置,则使用消息自己的路由键(fanout 模式不需要)$ttl_queue_message = 'ttl_queue_message';$channel->queue_declare($ttl_queue_message, false, true, false, false, false, $arguments);$channel->queue_bind($ttl_queue_message, $ttl_exchange, $ttl_routing_key);// 创建一个 TTL 的队列 (过期后不会死信消息!)$arguments = new AMQPTable();$arguments->set('x-expires', 10000); // 设置队列的过期时间 10s$arguments->set('x-dead-letter-exchange', $dead_exchange); // 设置处理死信消息的交换机$arguments->set('x-dead-letter-routing-key', $dead_routing_key); // 指定一个路由关键字用于死信消息。如果没有设置,则使用消息自己的路由键(fanout 模式不需要)$ttl_queue = 'ttl_queue';$channel->queue_declare($ttl_queue, false, true, false, false, false, $arguments);$channel->queue_bind($ttl_queue, $ttl_exchange, $ttl_routing_key);// 往队列中发送 10 条消息for ($i = 1; $i <= 10; $i++) { $msg = new AMQPMessage('AAA test test——' . $i, [ 'expiration' => 10000 // 设置每条消息过期时间。(当同时指定了每队列和每条消息的 TTL 时,将选择两者之间的较低值) ]); $channel->basic_publish($msg, $ttl_exchange, $ttl_routing_key);}$channel->close();$connection->close();\n\n运行 10s 后:\n","categories":["消息队列"],"tags":["RabbitMQ","消息队列","MQ","DLX"]},{"title":"RabbitMQ 简介","url":"/2022/01/35557bc0ca9a/","content":"\n官网地址:https://www.rabbitmq.com/tutorials/amqp-concepts.html\n\nRabbitMQ 简介RabbitMQ 是一个开源的遵循 AMQP 协议实现的基于 Erlang 语言编写,支持多种客户端(语言)。用于在分布式系统中存储消息,转发消息,具有高可用,高可扩性,易用性等特征。\nAMQPAMQP(Advanced Message Queuing Protocol 高级消息队列协议)是一个网络协议。它支持符合要求的客户端应用(application)和消息中间件代理(messaging middleware broker)之间进行通信。\nAMQP 0-9-1工作过程:消息(message)被发布者(publisher)发送给交换机(exchange),交换机常常被比喻成邮局或者邮箱。然后交换机将收到的消息根据路由规则分发给绑定的队列(queue)。最后AMQP代理会将消息投递给订阅了此队列的消费者,或者消费者按照需求自行获取。\n发布者:消费者:发布者(publisher)发布消息时可以给消息指定各种消息属性(message meta-data)。有些属性有可能会被消息代理(brokers)使用,然而其他的属性则是完全不透明的,它们只能被接收消息的应用所使用。\n从安全角度考虑,网络是不可靠的,接收消息的应用也有可能在处理消息的时候失败。基于此原因,AMQP 模块包含了一个消息确认(message acknowledgements)的概念:当一个消息从队列中投递给消费者后(consumer),消费者会通知一下消息代理(broker),这个可以是自动的也可以由处理消息的应用的开发者执行。当“消息确认”被启用的时候,消息代理不会完全将消息从队列中删除,直到它收到来自消费者的确认回执(acknowledgement)。\n在某些情况下,例如当一个消息无法被成功路由时,消息或许会被返回给发布者并被丢弃。或者,如果消息代理执行了延期操作,消息会被放入一个所谓的死信队列中。此时,消息发布者可以选择某些参数来处理这些特殊情况。\n队列,交换机和绑定统称为 AMQP 实体(AMQP entities)。\n一个可编译的协议:AMQP 0-9-1是一个可编程协议,某种意义上说AMQP的实体和路由规则是由应用本身定义的,而不是由消息代理定义。包括像声明队列和交换机,定义他们之间的绑定,订阅队列等等关于协议本身的操作。\n这虽然能让开发人员自由发挥,但也需要他们注意潜在的定义冲突。当然这在实践中很少会发生,如果发生,会以配置错误(misconfiguration)的形式表现出来。\n应用程序(Applications)声明 AMQP 实体,定义需要的路由方案,或者删除不再需要的 AMQP 实体。\n交换机和交换机类型交换机是消息发送到的 AMQP 0-9-1 实体。交换机接收消息并将其路由到零个或多个队列中。使用的路由算法取决于绑定的交换机类型和规则。AMQP 0-9-1 提供四种交换机类型:\n\n\n\n交换机类型\n默认预先声明的名称\n\n\n\nDirect\n(Empty string) and amq.direct\n\n\nFanout\namq.fanout\n\n\nTopic\namq.topic\n\n\nHeaders\namq.match (and amq.headers in RabbitMQ)\n\n\n除了交换类型之外,交换还声明了许多属性 :\n\nName\nDurability (交易所在代理重新启动后仍然有效)\nAuto-delete (当最后一个队列从中解绑时,将删除交换)\nArguments \n\n交换机可以有两个状态:持久(durable)、暂存(transient)。持久化的交换机会在消息代理(broker)重启后依旧存在,而暂存的交换机则不会(它们需要在代理再次上线后重新被声明)。然而并不是所有的应用场景都需要持久化的交换机。\n默认交换机默认交换机(default exchange)实际上是一个由消息代理预先声明好的没有名字(名字为空字符串)的直连交换机(direct exchange)。它有一个特殊的属性使得它对于简单应用特别有用处:那就是每个新建队列(queue)都会自动绑定到默认交换机上,绑定的路由键(routing key)名称与队列名称相同。\n举个栗子:当你声明了一个名为”search-indexing-online”的队列,AMQP代理会自动将其绑定到默认交换机上,绑定(binding)的路由键名称也是为”search-indexing-online”。因此,当携带着名为”search-indexing-online”的路由键的消息被发送到默认交换机的时候,此消息会被默认交换机路由至名为”search-indexing-online”的队列中。换句话说,默认交换机看起来貌似能够直接将消息投递给队列,尽管技术上并没有做相关的操作。\nDirect(直连交换机)直连型交换机(direct exchange)是根据消息携带的路由键(routing key)将消息投递给对应队列的。直连交换机用来处理消息的单播路由(unicast routing)(尽管它也可以处理多播路由)。下边介绍它是如何工作的:\n\n将一个队列绑定到某个交换机上,同时赋予该绑定一个路由键(routing key)\n当一个携带着路由键为R的消息被发送给直连交换机时,交换机会把它路由给绑定值同样为R的队列。\n\n直连交换机经常用来循环分发任务给多个工作者(workers)。当这样做的时候,我们需要明白一点,在 AMQP 0-9-1 中,消息的负载均衡是发生在消费者(consumer)之间的,而不是队列(queue)之间。\nFanout(扇出交换)扇出交换机 (fanout exchanges) 将消息路由到绑定到它的所有队列,并且忽略路由键。如果 N 个队列绑定到一个扇出交换器,则当一条新消息发布到该交换器时,该消息的副本将传递到所有 N 个队列。扇出交换是消息广播路由的理想选择。\n因为扇出交换向绑定到它的每个队列传递消息的副本,所以它的用例非常相似:\n\n大型多人在线 (MMO) 游戏可以将其用于排行榜更新或其他全球活动\n体育新闻网站可以使用扇出交换近乎实时地向移动客户端分发分数更新\n分布式系统可以广播各种状态和配置更新\n群聊可以使用扇出交换在参与者之间分发消息(虽然 AMQP 没有内置的出席概念,所以 XMPP 可能是更好的选择)\n\n\nTopic(主题交换机)主题交换机(topic exchanges)通过对消息的路由键和队列到交换机的绑定模式之间的匹配,将消息路由给一个或多个队列。主题交换机经常用来实现各种分发/订阅模式及其变种。主题交换机通常用来实现消息的多播路由(multicast routing)。\n主题交换机拥有非常广泛的用户案例。无论何时,当一个问题涉及到那些想要有针对性的选择需要接收消息的 多消费者/多应用(multiple consumers/applications) 的时候,主题交换机都可以被列入考虑范围。\n使用案例:\n\n分发有关于特定地理位置的数据,例如销售点\n由多个工作者(workers)完成的后台任务,每个工作者负责处理某些特定的任务\n股票价格更新(以及其他类型的金融数据更新)\n涉及到分类或者标签的新闻更新(例如,针对特定的运动项目或者队伍)\n云端的不同种类服务的协调\n分布式架构/基于系统的软件封装,其中每个构建者仅能处理一个特定的架构或者系统。\n\nHeader(头交换机)有时消息的路由操作会涉及到多个属性,此时使用消息头就比用路由键更容易表达,头交换机(headers exchange)就是为此而生的。头交换机使用多个消息属性来代替路由键建立路由规则。通过判断消息头的值能否与指定的绑定相匹配来确立路由规则。\n我们可以绑定一个队列到头交换机上,并给他们之间的绑定使用多个用于匹配的头(header)。这个案例中,消息代理得从应用开发者那儿取到更多一段信息,换句话说,它需要考虑某条消息(message)是需要部分匹配还是全部匹配。上边说的“更多一段消息”就是”x-match”参数。当”x-match”设置为“any”时,消息头的任意一个值被匹配就可以满足条件,而当”x-match”设置为“all”的时候,就需要消息头的所有值都匹配成功。\n头交换机可以视为直连交换机的另一种表现形式。头交换机能够像直连交换机一样工作,不同之处在于头交换机的路由规则是建立在头属性值之上,而不是路由键。路由键必须是一个字符串,而头属性值则没有这个约束,它们甚至可以是整数或者哈希值(字典)等。\n","categories":["消息队列"],"tags":["RabbitMQ","消息队列"]},{"title":"RabbitMQ-发布/订阅模式(Fanout)","url":"/2022/01/923b4f5b62dd/","content":"\n官网教程:https://www.rabbitmq.com/tutorials/tutorial-three-php.html\n\nRabbitMQ 中消息传递模型的核心思想是生产者从不直接向队列发送任何消息。实际上,生产者通常根本不知道消息是否会被传递到任何队列。\n生产者只能向交换器发送消息。交换是一件非常简单的事情。一方面它接收来自生产者的消息,另一方面它将它们推送到队列中。交换必须确切地知道如何处理它收到的消息。是否应该将其附加到特定队列?它应该附加到许多队列中吗?或者它应该被丢弃。其规则由 交换类型定义。\n简单模式 中使用的是默认类型交换机。\n\n生产者<?phprequire_once __DIR__ . '/vendor/autoload.php';use PhpAmqpLib\\Connection\\AMQPStreamConnection;use PhpAmqpLib\\Message\\AMQPMessage;// 创建服务器的连接$connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest');// 建立通道$channel = $connection->channel();// 创建交换机$exchange = 'fanout_exchange';$channel->exchange_declare($exchange, 'fanout', false, false, false);// 发布消息$data = 'Hello World!';$msg = new AMQPMessage($data);$channel->basic_publish($msg, $exchange);echo " [x] 发送 ", $data, "\\n";// 关闭通道和连接$channel->close();$connection->close();\n\n消费者<?phprequire_once __DIR__ . '/vendor/autoload.php';use PhpAmqpLib\\Connection\\AMQPStreamConnection;use PhpAmqpLib\\Message\\AMQPMessage;// 创建服务器的连接$connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest');$channel = $connection->channel();// 创建交换机 类型:direct、topic、headers 和 fanout$exchange = 'fanout_exchange';$channel->exchange_declare($exchange, 'fanout', false, false, false);// 队列名称为 '' 时,自动创建一个随机队列 (随着消费者的断开自动删除)list($queue_name, ,) = $channel->queue_declare("", false, false, true, false);/** * 交换机和队列绑定 * @pararm1 queue 队列名称 * @pararm2 exchange 交换机名称 * @pararm3 routing_key 路由 key (fanout 模式没有效果!) */$channel->queue_bind($queue_name, $exchange);echo ' [*] Waiting for logs. To exit press CTRL+C', "\\n";echo '消费队列:', $queue_name , "\\n";$callback = function ($msg) { echo ' [x] 接收 ', $msg->body, "\\n";};// 接收消息$channel->basic_consume($queue_name, '', false, true, false, false, $callback);while (count($channel->callbacks)) { $channel->wait();}// 关闭通道和连接$channel->close();$connection->close();\n\n运行运行多个消费者后,并运行生产者。\nC:\\Users\\Administrator\\Code\\base\\RabbitMQ>php receive_logs.php [*] Waiting for logs. To exit press CTRL+C消费队列:amq.gen-T23d5DY-KIxPjeiDkEJNOw [x] 接收 Hello World!\n\nC:\\Users\\Administrator\\Code\\base\\RabbitMQ>php receive_logs.php [*] Waiting for logs. To exit press CTRL+C消费队列:amq.gen-xEHdMTSElljtCqJnTC40xQ [x] 接收 Hello World!\n\nC:\\Users\\Administrator\\Code\\base\\RabbitMQ>php receive_logs.php [*] Waiting for logs. To exit press CTRL+C消费队列:amq.gen-p3Ok8B4BAurCiKLIFoFrQw [x] 接收 Hello World!\n\n","categories":["消息队列"],"tags":["RabbitMQ","消息队列"]},{"title":"RabbitMQ - 简单模式","url":"/2022/01/75c9a40f1420/","content":"\n官网教程:https://www.rabbitmq.com/tutorials/tutorial-one-php.html\n\n\n安装 php-amqplibcomposer require php-amqplib\n\n生成者<?phprequire_once __DIR__ . '/vendor/autoload.php';use PhpAmqpLib\\Connection\\AMQPStreamConnection;use PhpAmqpLib\\Message\\AMQPMessage;// 创建服务器的连接$connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest');// 建立通道$channel = $connection->channel();/** * 创建队列 * @pararm1 queue 队列名 * @pararm2 passive * @pararm3 durable 是否持久化 * @pararm4 exclusive 是否独有的 * @pararm5 auto_delete 是否自动删除 */$queue_name = 'queue1';$channel->queue_declare($queue_name, false, true, false, false);$data = 'Hello World';$msg = new AMQPMessage($data);/** * 发布消息 * @pararm1 msg 消息 * @pararm2 exchange 交换机名 * @pararm3 routing_key 路由 key (交换机为空时,用队列名) */$channel->basic_publish($msg, '', $queue_name);echo " [x] 发送 ", $data, " \\n";// 关闭通道和连接$channel->close();$connection->close();\n\n消费者<?phprequire_once __DIR__ . '/vendor/autoload.php';use PhpAmqpLib\\Connection\\AMQPStreamConnection;// 创建服务器的连接$connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest');// 建立通道$channel = $connection->channel();// 创建队列// 由于我们可能在生产者之前启动消费者,确保队列存在。$queue_name = 'queue1';$channel->queue_declare($queue_name, false, true, false, false);echo ' [*] Waiting for messages. To exit press CTRL+C', "\\n";$callback = function ($msg) { echo " [x] 接收 ", $msg->body, "\\n"; echo " [x] 完成", "\\n";};$channel->basic_consume($queue_name, '', false, false, false, false, $callback);while (count($channel->callbacks)) { $channel->wait();}\n\n运行\n查看 RabbitMQ 有哪些队列以及其中有多少消息\nadmin_s@SC-201811011347:~$ sudo rabbitmqctl list_queuesTimeout: 60.0 seconds ...Listing queues for vhost / ...name messageshello2 1queue_work2 0ttl_queue 0queue_work 0dead_queue 0hello1 0queue1 1\n\n","categories":["消息队列"],"tags":["RabbitMQ","消息队列"]},{"title":"RabbitMQ - 路由模式 (direct)","url":"/2022/01/61d2dd5762ab/","content":"\n官网教程:https://www.rabbitmq.com/tutorials/tutorial-four-php.html\n\n\n生产者<?phprequire_once __DIR__ . '/vendor/autoload.php';use PhpAmqpLib\\Connection\\AMQPStreamConnection;use PhpAmqpLib\\Message\\AMQPMessage;$connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest');$channel = $connection->channel();// 创建交换机 类型:di rect、topic、headers 和 fanout$exchange = 'direct_exchange';$channel->exchange_declare($exchange, 'direct', false, false, false);// 级别 'info'、'warning'、'error'$severity = isset($argv[1]) && !empty($argv[1]) ? $argv[1] : 'info';$data = implode(' ', array_slice($argv, 2));if(empty($data)) $data = 'Hello World!!';$msg = new AMQPMessage($data);// 设置路由 $severity$channel->basic_publish($msg, $exchange, $severity);echo " [x] 发送 ",$severity,':',$data," \\n";$channel->close();$connection->close();\n\n消费者<?phprequire_once __DIR__ . '/vendor/autoload.php';use PhpAmqpLib\\Connection\\AMQPStreamConnection;$connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest');$channel = $connection->channel();// 创建交换机 $exchange = 'direct_exchange';$channel->exchange_declare($exchange, 'direct', false, false, false);// 创建随机队列list($queue_name, ,) = $channel->queue_declare("", false, false, true, false);$severities = array_slice($argv, 1);if(empty($severities )) { file_put_contents('php://s tderr', "Usage: $argv[0] [info] [warning] [error]\\n"); exit(1);}// 为每个级别创建新绑定foreach($severities as $severity) { /** * @pararm1 queue 队列名称 * @pararm2 exchange 交换机名称 * @pararm3 routing_key 路由 key (fanout 模式没有效果!) */ $channel->queue_bind($queue_name, $exchange, $severity); echo $queue_name, ' 绑定 routingKey:', $severity, "\\n";}echo ' [*] Waiting for logs. To exit press CTRL+C', "\\n";$callback = function($msg){ echo ' [x] ',$msg->delivery_info['routing_key'], ':', $msg->body, "\\n";};$channel->basic_consume($queue_name, '', false, true, false, false, $callback);while(count($channel->callbacks)) { $channel->wait();}$channel->close();$connection->close();\n\n运行>php emit_log_direct.php error '发生一个错误' [x] 发送 error:'发生一个错误'>php emit_log_direct.php info 'Hello world!' [x] 发送 info:'Hello world!'>php emit_log_direct.php warning '警告!警告' [x] 发送 warning:'警告!警告'\n\n>php receive_logs_direct.php error warningamq.gen-Pl1yQBt6tjp-zCzNoucYXA 绑定 routingKey:erroramq.gen-Pl1yQBt6tjp-zCzNoucYXA 绑定 routingKey:warning [*] Waiting for logs. To exit press CTRL+C [x] error:'发生一个错误' [x] warning:'警告!警告'\n\n>php receive_logs_direct.php info warningamq.gen-SFe2hF_A7i9fjD5ynFUW9A 绑定 routingKey:infoamq.gen-SFe2hF_A7i9fjD5ynFUW9A 绑定 routingKey:warning [*] Waiting for logs. To exit press CTRL+C [x] info:'Hello world!' [x] warning:'警告!警告'","categories":["消息队列"],"tags":["RabbitMQ","消息队列"]},{"title":"Ubuntu 安装 RabbitMQ","url":"/2022/01/93c4cd7683c4/","content":"\n参考官网文档:\nhttps://www.rabbitmq.com/install-debian.htmlhttps://www.rabbitmq.com/install-debian.html#apt-quick-start-packagecloud\n\n启用 apt HTTPS 传输sudo apt-get install apt-transport-https\n\n添加存储库签名密钥## Team RabbitMQ's main signing keycurl -1sLf "https://keys.openpgp.org/vks/v1/by-fingerprint/0A9AF2115F4687BD29803A206B73A36E6026DFCA" | sudo gpg --dearmor | sudo tee /usr/share/keyrings/com.rabbitmq.team.gpg > /dev/null## Launchpad PPA that provides modern Erlang releasescurl -1sLf "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0xf77f1eda57ebb1cc" | sudo gpg --dearmor | sudo tee /usr/share/keyrings/net.launchpad.ppa.rabbitmq.erlang.gpg > /dev/null## PackageCloud RabbitMQ repositorycurl -1sLf "https://packagecloud.io/rabbitmq/rabbitmq-server/gpgkey" | sudo gpg --dearmor | sudo tee /usr/share/keyrings/io.packagecloud.rabbitmq.gpg > /dev/null\n签名指南\n添加源列表文件为了建立一个恰当的存储库来提供正确的包,需要做一些决策。一是确定发行版名称。它通常与使用的 Debian 或 Ubuntu 版本相匹配PackageCloud 上 RabbitMQ apt 存储库中应该使用的 OS 版本和发行版名称:\n\nfocal —— Ubuntu 20.04\nbionic —— Ubuntu 18.04\nbuster —— Debian Buster\nbuster —— Debian Bullseye\nbuster —— Debian Sid\n\nsudo tee /etc/apt/sources.list.d/rabbitmq.list <<EOF## Provides modern Erlang/OTP releases#### "bionic" as distribution name should work for any reasonably recent Ubuntu or Debian release.## See the release to distribution mapping table in RabbitMQ doc guides to learn more.deb [signed-by=/usr/share/keyrings/net.launchpad.ppa.rabbitmq.erlang.gpg] http://ppa.launchpad.net/rabbitmq/rabbitmq-erlang/ubuntu bionic maindeb-src [signed-by=/usr/share/keyrings/net.launchpad.ppa.rabbitmq.erlang.gpg] http://ppa.launchpad.net/rabbitmq/rabbitmq-erlang/ubuntu bionic main## Provides RabbitMQ#### Replace $distribution with the name of the Ubuntu release used.## On Debian, "deb/ubuntu" should be replaced with "deb/debian"deb [signed-by=/usr/share/keyrings/io.packagecloud.rabbitmq.gpg] https://packagecloud.io/rabbitmq/rabbitmq-server/ubuntu/ $distribution maindeb-src [signed-by=/usr/share/keyrings/io.packagecloud.rabbitmq.gpg] https://packagecloud.io/rabbitmq/rabbitmq-server/ubuntu/ $distribution mainEOF\n其中 $distribution 修改为 Debian 或 Ubuntu 发行版的名称。\n例:Ubuntu 20.04 中\nsudo tee /etc/apt/sources.list.d/rabbitmq.list <<EOF## Provides modern Erlang/OTP releases#### "bionic" as distribution name should work for any reasonably recent Ubuntu or Debian release.deb [signed-by=/usr/share/keyrings/net.launchpad.ppa.rabbitmq.erlang.gpg] http://ppa.launchpad.net/rabbitmq/rabbitmq-erlang/ubuntu bionic maindeb-src [signed-by=/usr/share/keyrings/net.launchpad.ppa.rabbitmq.erlang.gpg] http://ppa.launchpad.net/rabbitmq/rabbitmq-erlang/ubuntu bionic main## Provides RabbitMQ##deb [signed-by=/usr/share/keyrings/io.packagecloud.rabbitmq.gpg] https://packagecloud.io/rabbitmq/rabbitmq-server/ubuntu/ focal maindeb-src [signed-by=/usr/share/keyrings/io.packagecloud.rabbitmq.gpg] https://packagecloud.io/rabbitmq/rabbitmq-server/ubuntu/ focal mainEOF\n\n更新 aptsudo apt-get update -y\n\n安装 Erlang## Install Erlang packagessudo apt-get install -y erlang-base \\ erlang-asn1 erlang-crypto erlang-eldap erlang-ftp erlang-inets \\ erlang-mnesia erlang-os-mon erlang-parsetools erlang-public-key \\ erlang-runtime-tools erlang-snmp erlang-ssl \\ erlang-syntax-tools erlang-tftp erlang-tools erlang-xmerl\n\n安装 rabbitmq-server 及其依赖sudo apt-get install rabbitmq-server -y --fix-missing\n\n启动测试sudo service rabbitmq-server start\n\n* Starting message broker rabbitmq-server [ OK ]\n","categories":["消息队列"],"tags":["RabbitMQ","消息队列"]},{"title":"消息队列常用协议","url":"/2022/01/633103713ffc/","content":"\n原文地址:https://www.kuangstudy.com/zl/rabbitmq#1366029180994654209\n\nAMQP 协议支持者者: RabbitMQ、ACTIVEMQ\n\nAMQP:(全称:Advanced Message Queuing Protocol)是高级消息队列协议。由摩根大通集团联合其他公司共同设计。是一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同的开发语言等条件的限制。Erlang中的实现有RabbitMQ等。特性:1.分布式事务支持。2.消息的持久化支持。3.高性能和高可靠的消息处理优势。\nMQTT 协议支持者: RabbitMQ、ACTIVEMQ\n\nMQTT协议:(Message Queueing Telemetry Transport)消息队列是IBM开放的一个即时通讯协议,物联网系统架构中的重要组成部分。特点:1.轻量2.结构简单3.传输快,不支持事务4.没有持久化设计。应用场景:1.适用于计算能力有限2.低带宽3.网络不稳定的场景。\nOpenMessage 协议支持者: Apache RocketMQ\n\n是近几年由阿里、雅虎和滴滴出行、Stremalio 等公司共同参与创立的分布式消息中间件、流处理等领域的应用开发标准。特点:1:结构简单2:解析速度快3:支持事务和持久化设计。\nKafka 协议支持者:Kafka\n\nKafka协议是基于TCP/IP的二进制协议。消息内部是通过长度来分割,由一些基本数据类型组成。特点是:1:结构简单2:解析速度快3:无事务支持4:有持久化设计\n","categories":["消息队列"],"tags":["消息队列"]},{"title":"跨域资源共享(CORS)","url":"/2020/12/e8478690d26b/","content":"什么是跨域资源共享(CORS)跨域资源共享是一种基于 HTTP 头的机制,该机制通过允许服务器标示除了自己以外的其他 origin (域、协议和端口),浏览器可以访问加载这些资源。\n跨域资源共享还通过一种机制来检查服务器是否允许要发送真实请求,该机制通过浏览器发送一个到服务器托管的跨域源资源的 预检 请求。预检 中浏览器发送的头中标示有 HTTP 方法和真实请求中会用到的头。\n\n在 http://domain-a.com 页面中对 https://domain-b.com 发送一个请求\n\n跨域资源共享标准新增一组 HTTP 首部字段,允许服务器声明哪些源站通过浏览器有权限访问哪些资源。另外,对于那些可能对服务器数据产生副作用的 HTTP 方法( GET 以外的 HTTP 请求,或者搭配某些 MIME类型 的 POST 请求),浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request),获知服务端是否允许该跨源请求。服务器确认允许之后,才发起实际的 HTTP 请求。\n场景简单请求不会触发 CORS 预检请求。若请求满足以下条件,则视为“简单请求”:\n\n使用 GET、HEAD、POST。\n除了类用户代理自动设置的首部字段(Connection、User-Agent)和在 Fetch 规范中定义为 禁用首部名称 的其他首部,允许人为设置的字段为 Fetch 规范定义的对 CORS 安全的首部字段集合:\nAccept\nAccept-Language\nContent-Language\nContent-Type\n\n\nContent-Type 的值仅限于下列三者之一:\ntext/plain\nmultipart/form-data\napplication/x-www-form-urlencodedvar\n\n\n请求中的任意 XMLHttpRequest 对象均没有注册任何事件监听器;XMLHttpRequest 对象可以使用 XMLHttpRequest.upload 属性访问\n请求中没有使用 ReadableStream 对象。\n\n测试一个简单请求:\n\n客户端 http://test-a.test :var xhr = new XMLHttpRequest();var url = 'http://test-b.test/test-cors';xhr.open('post', url);xhr.send();\n服务端 http://test-b.test:响应头增加: Access-Control-Allow-Origin: *\n\n \n对于简单请求,只需在服务器响应中添加 Access-Control-Allow-Origin: *,可以被任意外域访问。\n预检请求 (浏览器自动发送)用于检查服务器是否支持 CORS 即跨域资源共享,使用 OPTIONS 方法发起一个预检请求到服务器。\n一般请求头部会包含: Access-Control-Allow-Method 、Access-Control-Allow-Headers、Origin\n测试\n\n客户端:var xhr = new XMLHttpRequest();var url = 'http://test-b.test/test-cors';xhr.open('post', url);xhr.setRequestHeader('Content-Type', 'application/xml'); // 增加 首部字段xhr.send();\n\n\n如果现在不修改服务端代码,会怎么样?\n控制台报错:Access to XMLHttpRequest at ‘http://test-b.test/test-cors' from origin ‘http://test-a.test' has been blocked by CORS policy: Request header field content-type is not allowed by Access-Control-Allow-Headers in preflight response.\n预检请求的响应 Access-Control-Allow-Headers 中不允许请求头部有 Content-type 字段\n\n\n服务端:\n需再在响应头部增加 Access-Control-Allow-Headers: Content-type\n额外: \n\nAccess-Control-Allow-Methods:服务器允许客户端使用什么方法发起请求\nAccess-Control-Max-Age:响应的有效时间内浏览器不需要为同一请求再发送预检请求。\n\n\n\n\n\n注意:\n预检请求的成功仅限于 200~299 状态,其他状态会导致不会被共享或使 CORS 预检请求失败\n\n附带身份凭证的请求\n当发出跨源请求时,第三方 cookie 策略仍将适用\n\n测试:\n\n客户端:var xhr = new XMLHttpRequest();var url = 'http://test-b.test/test-cors';xhr.open('post', url);xhr.withCredentials = true; // 发送凭证信息xhr.send();\n\n\n不修改服务端的情况下发送请求尝试\n控制台报错:The value of the ‘Access-Control-Allow-Origin’ header in the response must not be the wildcard ‘*’ when the request’s credentials mode is ‘include’. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.\n意思大体就是当请求附带凭证时响应中 Access-Control-Allow-Origin 的值不能是 * ,而且还告诉说 XMLHttpRequest 发起带身份凭据的请求是 withCredentials 属性控制的。\n\n\n服务端:响应头部修改 Access-Control-Allow-Origin: http://test-a.test,并且还需增加 Access-Control-Allow-Credentials: true。\n\n\n\n注意:\n在响应附带身份凭证的请求时:服务器不能将 Access-Control-Allow-Origin 、Access-Control-Allow-Headers 、Access-Control-Allow-Methods 的值设置为 * 。\n\n总结HTTP 响应头部字段\nAccess-Control-Allow-Origin:允许访问该资源的外域 URI,如果值为具体域名,则在响应首部中 Vary 字段值必须包含 Origin\nAccess-Control-Allow-Headers:请求所允许使用的 HTTP 方法\nAccess-Control-Allow-Methods:求中允许携带的首部字段\nAccess-Control-Max-Age:缓存预检请求结果\nAccess-Control-Allow-Credentials:决定请求是否可以使用 credentials\nAccess-Control-Expose-Headers:控制 XMLHttpRequest 对象的 getResponseHeader() 方法获得除基本响应头(Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma)以外的响应头。\n\n\n\n\n参见:跨域资源共享 、 https://fetch.spec.whatwg.org/#http-cors-protocol\n","categories":["网络"],"tags":["笔记","CORS"]},{"title":"PHPStorm 启动时报错:No JVM installation found","url":"/2022/10/99b5e82eecaf/","content":"今天,打开PHPStorm时突然报错,如下:\n查看JDK版本确实为64位:\n又查看PHPStorm 版本,果然发现问题,根据桌面快捷方式打开文件位置\n双击phpstorm64.exe,正常打开软件。\n","categories":["运维"],"tags":["phpstrom"]},{"title":"安装Ubuntu困扰我两天的问题","url":"/2022/05/77cdd20acacf/","content":"\n近日在给物理机服务器安装 Ubuntu 系统的时候,一直卡死在这个报错中。\n\n\n由于不是专业运维,无法直接定位错误,只知道是磁盘的读写出现了问题。百度谷歌了一下,大多是都说是磁盘坏掉了,或者是插槽松了。\n经过一番折腾,还是一直出现这个错误…… 因为是公司全新的机器,硬盘应该是不能坏的,继续查阅资料……\n就在绝望之际,看到了一篇说是可能 RAID 故障了,需要重做一下。\nRAID 是啥?之前安装系统也没搞过这个呀!\n\nRAID ( Redundant Array of Independent Disks )即独立磁盘冗余阵列,简称为「磁盘阵列」,其实就是用多个独立的磁盘组成在一起形成一个大的磁盘系统,从而实现比单块磁盘更好的存储性能和更高的可靠性。\n\n然后查了一下 “重做 RAID”,检索到的结果大部分都是 “ DELL …… 重做 RAID……”,我就是 DELL的 😅,难道是 DELL 的都需要?\n开整\n启动/重启服务器 ——》 F2 进入 BIOS \n\n——》 选择 Device settings\n\n——》 找到带有 RAID 的选项 ——》 Configuration Management\n\n\n——》 找到 Clear 清理一下 \n[无图]\n——》 Create Virtual Disk\n\n\n注意这个 RAID Level,其中有 Raid0、 Raid1、 Raid5、 Raid10。Raid 0:至少需要两块硬盘,磁盘越多,读写速度越快,没有冗余。Raid 1:只能用两块硬盘,两块硬盘的数据互为镜像(写慢,读快),一块磁盘冗余。Raid 5:至少需要3块硬盘,一块磁盘冗余。它是最通行的配置方式。具有奇偶校验的数据恢复功能的数据存贮方式。奇偶校验数据块分布于阵列里的各个硬盘中。Raid 6:至少需要4块硬盘,2块磁盘冗余,硬盘的总数大于等于4即可。Raid 10:至少需要4块硬盘,冗余一半的硬盘数量,但是硬盘的总数必须是大于或等于4的偶数(相当于每两块硬盘做一个Raid0,然后把各个Raid0做成一个Raid1)。\n\n我选择的是 RAID0。\n——》 Select Physical Disks 选择硬盘\n\n——》根据硬盘类型选择,我的是 HDD。然后选择硬盘。\n\n\n我机器上的硬盘其实是4块 500G 的硬盘,但是这里只显示了一块这里需要返回之前的创建页面清理一下配置 Clear Configuration\n\n\n清理完成后:\n——》 Apply Change ——》 Create Virtual Disk\n完成后,返回重新进入,会看到 View Disk Group Properties,说明成功!\n重启,安装系统!\n","categories":["运维"],"tags":["ubuntu"]},{"title":"宝塔面板:解析软件列表发生错误,已尝试自动修复,请刷新页面重试!","url":"/2022/05/70d45bff41a4/","content":"宝塔面板报错:cmake: /usr/local/lib/libcurl.so.4: no version information available (required by cmake)\n查找 libcurl.so.4 路径> whereis libcurl.so.4libcurl.so: /usr/lib/x86_64-linux-gnu/libcurl.so /usr/lib/x86_64-linux-gnu/libcurl.so.4 /usr/local/lib/libcurl.so /usr/local/lib/libcurl.so.4\n查看 libcurl.so.4 的指向> ll /usr/local/lib/libcurl.so.4lrwxrwxrwx 1 root root 16 May 30 16:20 /usr/local/lib/libcurl.so.4 -> libcurl.so.4.6.0*\n删除错误的软链,设置软链 /usr/lib/x86_64-linux-gnu/libcurl.so.4sudo rm -rf /usr/local/lib/libcurl.so.4sudo ln -s /usr/lib/x86_64-linux-gnu/libcurl.so.4 /usr/local/lib/libcurl.so.4\n完成!\n\n","categories":["运维"],"tags":["宝塔","BT"]}]