遇到解析微信的 语音文件 需要将amr格式转换为mp3格式
然后搜了一下 基本上就是基于ffmpeg来处理就行
这里记录一下 方便自己速查
1 | ffmpeg -i xxx.amr -vn -acodec libmp3lame -f mp3 -y xxx.mp3 |
使用jave 参考:https://developer.aliyun.com/ask/77316?spm=a2c6h.13159736
1 | <!-- https://mvnrepository.com/artifact/it.sauronsoftware/jave --> |
1 | import it.sauronsoftware.jave.AudioAttributes; |
使用dadiyang 1.0.6的依赖有点问题 降级1.0.5 即可
1 | <!-- https://mvnrepository.com/artifact/com.github.dadiyang/jave --> |
1 | package com.ming.service; |
都是ffmpeg 处理的
看看ffmpeg 然后借助一些封装的jar使用就行
核心还是ffmpeg
在一些比较不同的公司中 没有提供java的sdk 需要调用dll so库等 需要用java-jni来调用
需要手动加载对应的lib到 内存中
例如linux下加载 xxx.so 库
直接在os目录中的 直接加载就是的
1 | //当前 库已经加入到系统环境变量中 直接按照名称加载就行 |
既然是在java中 肯定是想打包进jar中 方便分发
这里以 maven管理项目的方式来演示
将 xxx.so 放进resources目录下 也就是 classLoader.getResource(“”)
然后在启动加载的时候 直接从jar中将库文件复制到os目录中 然后加载
1 | package com.ming.test; |
简单记录一下 加载dll so库的方式
基本上就是直接通过环境变量+库名加载 或者直接通过绝对路径加载
如果是在jar中 就加载class的时候 复制到os的目录然后在加载
一直听说过 Let`s Encrypt 自动签发证书 一直用的阿里云申请的免费证书 一年换一次 鸡儿痛
最近干脆切换到 certBot 来自动申请管理Let`s Encrypt 证书
https://certbot.eff.org/ certbot官网
以在debian上 nginx+certBot 自动签发管理为例
1 | echo "deb http://nginx.org/packages/debian/ bullseye nginx">> /etc/apt/sources.list |
https://certbot.eff.org/ 在官网选择 nginx或者其他系统和操作系统及其安装方式
1 | debian 默认是 snap方式 不想用 直接安装 python版本 pip安装 没使用官方的文档 直接搜索 deb仓库里面的 |
配置文件在/etc/nginx/conf.d 新增show.conf
1 | server { |
如果是国内必须确定是否可以访问443端口 自动配置会验证443端口可用性
例如未备案被拦截 或者被阻止访问 都无法申请成功
1 | 开始自动配置 会要求输入邮箱 选择各种方式 按照提示选择即可 实在不会 参考: https://www.jianshu.com/p/4d4f9376fe20 |
1 | server { |
1 | curl https://show.xujiuming.com |
1 | 模拟运行 自动续期 |
#给nginx自动配置 certbot --nginx # 指定域名 certbot --nginx -d \[domain]# 续期 certbot renew # 测试续期 certbot renew --dry-run # helpcertbot --help
certBot 自动处理证书相关配置 简单粗暴 适合懒狗使用
]]>在jdk8之前 如果要使用date 的类型 就需要把java.util.Date 之类的类组合起来使用
特别是在常用的时间加减、获取特定时间、格式化的时候 都必须要java.util.Calendar 、java.util.TimeZone、java.text.SimpleDateFormat这些类进行封装 或者需要引入第三方工具包来简化操作
在jdk8中 time是以jodaTime这个工具包为模板 加入到jdk中的 对于时间的加减、获取特定时间、格式化、变更时区、获取更加精确的时间等等操作变的更加简单 而且是不可变并且线程安全的方法
名称 | 功能 | 备注 |
---|---|---|
Instant | 获取从1970年开始的时间点 | 类似之前的java.util.Date、通过这个类可以获取到非常精确的时间 |
LocalDate | 本地年月日Date对象 | 获取当前年月日信息的对象 |
LocalTime | 本地时分秒Date对象 | 获取当前时分秒信息的对象 |
LocalDateTime | 本地年月日时分秒Date对象 | 获取当前年月日时分秒信息的对象 |
OffsetTime | 获取时分秒Date对象并且带上偏移时间信息 | |
OffsetDateTime | 获取带年月日时分秒的Date对象并且带上偏移时间信息 | |
ZonedDateTime | 获取带时区信息的年月日时分秒对象 并且带上时区信息 | |
Year | 年对象 | |
YearMonth | 年月对象 | |
MonthDay | 月日对象 | |
Period | 时间间隔区间对象 | 表示以年、月、日衡量的时长 |
Duration | 时间间隔区间对象 | 表示以秒和纳秒为基准的时长 |
名称 | 含义 | 备注 |
---|---|---|
NANOS | 纳秒 | |
MICROS | 微秒 | |
MILLIS | 毫秒 | |
SECONDS | 秒 | |
MINUTES | 分 | |
HOURS | 小时 | |
HALF_DAYS | 半天 | 12个小时 |
DAYS | 一天 | 24个小时 |
WEEKS | 一周 | 7天 |
MONTHS | 一个月 | |
YEARS | 一年 | |
DECADES | 十年 | |
CENTURIES | 百年 | |
MILLENNIA | 千年 | |
ERAS | 十亿年 | |
FOREVER | 永远 | Long.MAX_VALUE |
名称 | 表达式 | 备注 |
---|---|---|
ISO_LOCAL_DATE | yyyy-MM-dd | |
ISO_OFFSET_DATE | yyyy-MM-dd+offset | |
ISO_DATE | ‘yyyy-MM-dd’ or ‘yyyy-MM-dd+offset’ | |
ISO_LOCAL_TIME | HH:mm or HH:mm:ss | |
ISO_OFFSET_TIME | HH:mm+offset or HH:mm:ss+offset | |
ISO_TIME | HH:mm or HH:mm:ss or HH:mm:ss+offset | |
ISO_LOCAL_DATE_TIME | yyyy-MM-ddTHH:mm:ss | |
ISO_OFFSET_DATE_TIME | yyyy-MM-ddTHH:mm:ss+offset | |
ISO_ZONED_DATE_TIME | yyyy-MM-ddTHH:mm:ss+offset[zone] | |
ISO_ORDINAL_DATE | yyyy-days | |
ISO_WEEK_DATE | yyyy-week-days | |
ISO_INSTANT | yyyy-MM-ddTHH:mm:ssZ | |
BASIC_ISO_DATE | yyyyMMdd | |
RFC_1123_DATE_TIME | ‘Tue, 3 Jun 2008 11:05:30 GMT’ |
名称 | 功能 | 备注 |
---|---|---|
of | 根据传入的数值转换成相应的时间对象 | |
parse | 根据传入的字符串格式和DateTimeFormatter枚举转换成相应的时间对象 | |
get | 根据时间对象获取相应的时间属性 | |
is | 判断时间的某些属性是否符合方法的意义 | 使用isBefore或者isAfter来判断时间的前后 |
with | 获取一些特殊时间对象 | 例如这个月第一天之类的 |
plus | 时间相加 | 可以根据不同的时间单位进行相加 |
minus | 时间相减 | 可以根据不同的时间单位进行相减 |
to | 时间类型转换成其他时间类型 | 例如LocalDateTime to 成 LocalTime |
at | 转换成带偏移量、时区之类的时间对象操作 | |
format | 格式化时间类型 | 根据DateTimeFormatter对象来转换 |
1 | package com.ming.admin; |
时间对象的操作在jdk8之前 其实很操蛋 只能通过使用一些自己封装或者 一些组织封装的dateUtils 来操作 有时候一些特殊的时间处理只能单独写工具了 很麻烦
现在jdk8 的time包 直接继承了jodaTime的操作 常用操作变成了 不可变而且线程安全的操作了 并且增强了对时间的 偏移和时区的处理 增加了 很多常规的时间处理方法
有点蛋疼的是需要考虑 框架之类的对于time包的兼容 特别是jdbc对于time包的对象的支持程度
不过 我可以使用Date转换成time的时间对象 在进行操作 然后再转换回去即可
redis+内存做二级缓存是很常见的套路了 但是一直有不太好处理的地方 例如 多客户端下 本地缓存和服务端缓存一致性问题
redis在6.x之后 增加了一个功能 client side caching tracking
https://lettuce.io/core/6.0.0.RC1/api/io/lettuce/core/support/caching/ClientSideCaching.html lettuce客户端接入实例
https://redis.io/docs/manual/client-side-caching/#other-hints-for-implementing-client-libraries redis client side caching tracking 官方文档
https://stackoverflow.com/questions/64885694/how-to-configure-client-side-caching-in-lettuce-6-spring-boot-2-4
https://redis.io/docs/manual/client-side-caching/#other-hints-for-implementing-client-libraries
在默认模式下,服务器会记住给定客户端访问的密钥,并在修改相同的密钥时发送失效消息。这会消耗服务器端的内存,但仅针对客户端可能在内存中拥有的密钥集发送失效消息。
1 | 1. 客户端启用tracking |
1 | 1. 服务器会记住可能在单个全局表中缓存给定密钥的客户端列表。此表称为失效表。失效表可以包含最大条目数。如果插入了新密钥,服务器可能会通过假装该密钥已被修改(即使未被修改)并向客户端发送失效消息来逐出较旧的条目。这样做,它可以回收用于此密钥的内存,即使这会强制具有密钥本地副本的客户端逐出它。 |
https://redis.io/docs/reference/protocol-spec/
https://github.com/redis/redis-doc/blob/master/docs/reference/protocol-spec.md
https://blog.csdn.net/LZH984294471/article/details/114233835
在广播模式下,服务器不会尝试记住给定客户端访问的密钥,因此此模式在服务器端根本不会消耗内存。相反,客户端订阅键前缀(如 或 ),并在每次触摸与订阅前缀匹配的键时收到通知消息。object:user:
如果没有指定 前缀 则发送所有修改的key消息 如果使用 N个前缀 会匹配对应的前缀的key修改消息发送 注意是前缀匹配 如果存在前缀覆盖了 也会匹配上 例如 foo 和food前缀
如果不想接受key失效消息 使用 【NOLOOP】模式
广播模式客户端实现的时候 要注意在 断开连接、心跳异常等情况清理本地缓存
和使用比较合理的本地缓存实现 例如使用guava caffeine 之类的 不要直接使用简易map
使用 BCAST模式 几乎不消耗内存 非广播模式使用的内存和跟踪的key数量和客户端数量成正比
https://www.lanmper.cn/redis/t9524 client tracking 命令
1 | CLIENT TRACKING ON|OFF[REDIRECT client-id][PREFIX prefix[PREFIX prefix ...]][BCAST][OPTIN][OPTOUT][NOLOOP] |
使用lettuce接入 client side cache tracking
使用map或者自定义的cache缓存实现
1 | package com.ming.admin.cache; |
多级缓存 很多情况下都用 就是有时候实现很纠结
redis在6.x之后增加了相关支持 用redis+内存实现两级缓存变的更加简单方便了
一直在用wsl2-ubuntu 感觉没劲 干脆重装arch 记录一下使用的指令 方便速查
wsl常用命令: https://learn.microsoft.com/zh-cn/windows/wsl/basic-commands?source=recommendations
wsl2 安装arch大致有两种做法 1-直接下载打包好的wsl2-arch 2-使用LxRunOffline安装
此处偷懒 直接使用打包好的 不使用LxRunOffline
管理员权限下执行
参考文档:https://docs.microsoft.com/zh-cn/windows/wsl/install
1 | 开启wsl |
1 | 进入arch |
1 | pacman-key --init |
1 | 初始化root的密码 |
安装一些基础工具
1 | sudo pacman -Syyu gcc git make unzip zip vim python python-pip screenfetch tree openssh vi wget tmux |
安装yay
makepkg 必须在非root用户执行
1 | clone yay代码 |
常用wsl命令
1 | 查看当前wsl镜像列表 |
常用组件安装
1 | 安装oh my zsh |
参考资料:
https://devblogs.microsoft.com/commandline/systemd-support-is-now-available-in-wsl/#set-the-systemd-flag-set-in-your-wsl-distro-settings
https://learn.microsoft.com/zh-cn/windows/wsl/wsl-config#wslconf
.wslconfig ,用于在 WSL 2 上运行的所有已安装分发版 全局 配置设置。
wsl.conf 为 WSL 1 或 WSL 2 上运行的 Linux 发行版配置 每个分发 版的设置。
1 | echo '[boot] |
1 | [boot] |
重启wsl 在powershell中输入 【 wsl –shutdown】 关机之后等8s
查看启动的systemd情况 【systemctl】
1 | wsl version >= 1.1.0 |
1 | sudo apt update |
使用wsl gpu加速 要去查看和验证当前显卡和驱动 是否支持
wsl 安装arch 各种大神 已经打包了很多方式了
安装完成之后 还需要做一些调整适配 安装zsh、开发环境之类的
记录一下 arch 自己安装的过程 免得每次都现查 看不到合适的文章
主要参考 arch wiki
安装arch系统 也跟其他系统一样
准备启动盘 引导、分区、安装基本系统、安装组件、配置 只不过要全手动去选择配置
https://wiki.archlinux.org/title/Iwd_(%E7%AE%80%E4%BD%93%E4%B8%AD%E6%96%87)
1 | 进入iwd |
1 | rfkill list |
分区 一般来说 新的电脑 如果是全盘格式化 需要格式化一个efi区域和系统盘 就行 如果有需要可以分一个swap
个人比较懒 就一块500g固态 直接分一个 500MB的efi分区 剩下全系统盘
查看分区信息
1 | 看硬件 一般是 /dev/sdaX 或者/dev/nvme0n1 这样的 我的笔记本是nvme0n1 也就是nvme的固态第一个硬盘 |
分区
分区有很多工具 我比较喜欢用cfdisk
1 | cfdisk /dev/nvme0n1 |
格式化
文件系统 选择一个喜欢用的就行 ext4 、zfs、 btrfs 个人选择ext4
1 | 格式化 efi盘 |
连接ntp服务
1 | timedatectl set-ntp true |
挂载分区
/mnt 为以后arch系统的/分区 /mnt/boot 对应/boot 为efi分区
1 | 注意 先挂载系统分区 |
选择合适的镜像源
1 | 自动选择 中国区域 前十的镜像源 |
安装基本系统和工具
1 | arch 的最基本的系统 |
写入分区信息
1 | 使用uuid模式 |
登录到新安装的系统
1 | arch-chroot /mnt |
设置时区信息
1 | 建立上海的时区的软连接 |
设置本地化信息
1 | 编辑本地化信息 将常见的 en_US zh_CN 等配置为UTF-8 |
配置主机名
1 | 第一行写入主机名 ming |
设置root密码
1 | passwd |
安裝基本组件
1 | pacman -S iwd networkmanager vi vim zsh |
安装微指令
1 | intel cpu |
安装grub和配置
1 | pacman -S os-prober grub efibootmgr |
设置必要开机启动
1 | 设置网络管理器 |
重启
1 | 卸载 |
断电之后拔u盘 然后启动的时候 看到grub引导页面 选择第一个就行
选择合适的源
1 | 自动选择 中国区域 前十的镜像源 |
新增用户
1 | 新增ming 用户组 |
安装显卡驱动
1 | pacman -S xf86-video-amdgpu mesa |
安装gnome
个人比较习惯 gnome
https://wiki.archlinux.org/title/GNOME_(%E7%AE%80%E4%BD%93%E4%B8%AD%E6%96%87)
1 | 安装gnome |
增加中文社区仓库
在/etc/pacman.d/mirrorlist中增加
1 | [archlinuxcn] |
1 | sudo pacman -Syy |
安装aur助手
yay paru 都可以 yay粗暴点
1 | 配置了中文社区仓库 可以直接安装 |
中文化
安装字体和输入法
1 | 安装中文字体 |
安装过很多次 arch 一直懒的记笔记
就是一个系统安装 按照wiki来
想部署一个k8s实验环境
看了很多 minikube kind k3s 发现还是rancher的k3s对资源要求少
k3s 本质上就是一个独立的可执行文件 包含整个k8s的manager的组件
大于1g的内存和能访问github站点的权限和安装好docker
https://rancher.com/docs/k3s/latest/en/installation/install-options/
1 | 官方脚本部署 |
https://rancher.com/docs/k3s/latest/en/installation/kube-dashboard/
安装dashboard
1 | GITHUB_URL=https://github.com/kubernetes/dashboard/releases |
创建rbac角色权限配置
dashboard.admin-user.yml
1 | apiVersion: v1 |
dashboard.admin-user-role.yml
1 | apiVersion: rbac.authorization.k8s.io/v1 |
部署配置和生成token
1 | 部署角色权限配置 |
通过转发代理配置远程访问
https://kubernetes.io/docs/tasks/access-application-cluster/port-forward-access-application-cluster/
https://segmentfault.com/a/1190000023130407
1 | 查看部署的service的端口 |
升级仪表板链接
1 | sudo k3s kubectl delete ns kubernetes-dashboard |
删除仪表板和管理员用户配置链接
1 | sudo k3s kubectl delete ns kubernetes-dashboard |
k3s当前版本使用最新的helm即可
https://helm.sh/docs/
https://helm.sh/docs/intro/install/
服务器版本是ubuntu
1 | curl https://baltocdn.com/helm/signing.asc | sudo apt-key add - |
直接设置在当前shell或者写入 .bashrc .zshrc之类的地方都可以 只要当前shell 有KUBECONFIG变量就行
1 | export KUBECONFIG=/etc/rancher/k3s/k3s.yaml |
k8s 安装各种应用 可以直接去编写 yaml来实现 也可以直接使用helm这种工具
这里演示 就直接使用helm来
1 | 添加指定仓库 |
k3s 总体安装使用 感觉还行 符合我的预期
简单粗暴 然后和k8s 区别不大
spring虽然也有事件 但是麻烦
大多数情况下 guava的event足够使用 需要高性能 可以采用disruptor 參考: disruptor使用笔记 基于disruptor实现简单topic分发消息功能
1 | package com.ming.service.event; |
1 | package com.ming.service.event; |
1 | package com.ming.service.event; |
1 | package com.ming.service.event; |
注意 @Subscribe 表明订阅事件 @AllowConcurrentEvents 表明并发安全 然后内部是根据event类型来分发到不同的订阅方的
1 | package com.ming.service.event; |
1 | package com.ming.service.core; |
guava 的event 简单粗暴
需要简单明了的事件处理 使用 guava的 event
需要高性能的 可以使用disruptor自己实现一套简单的
懒得自己手写api
干脆让他根据包名、类名 函数名自动生成算了
1 | package com.ming.base.mvc.annotation; |
1 | package com.ming.base.mvc; |
1 | package com.ming.controller; |
直接看输出日志
2021-10-11 11:12:35.433 [restartedMain] INFO com.ming.base.mvc.ControllerAutoRegister- auto register api......2021-10-11 11:12:35.439 [restartedMain] INFO com.ming.base.mvc.ControllerAutoRegister- scan rest controller number:12021-10-11 11:12:36.257 [restartedMain] INFO com.ming.base.mvc.ControllerAutoRegister- register controller number:12021-10-11 11:12:36.284 [restartedMain] INFO com.ming.base.mvc.ControllerAutoRegister- register api :【GET】["/api/test/get"],com.ming.controller.TestController#get2021-10-11 11:12:36.284 [restartedMain] INFO com.ming.base.mvc.ControllerAutoRegister- register api :【PUT】["/api/test/put"],com.ming.controller.TestController#put2021-10-11 11:12:36.285 [restartedMain] INFO com.ming.base.mvc.ControllerAutoRegister- register api :【DELETE】["/api/test"],com.ming.controller.TestController#delete2021-10-11 11:12:36.285 [restartedMain] INFO com.ming.base.mvc.ControllerAutoRegister- register api :【OPTIONS】["/api/test/options"],com.ming.controller.TestController#options2021-10-11 11:12:36.285 [restartedMain] INFO com.ming.base.mvc.ControllerAutoRegister- register api :【PATCH】["/api/test/patch"],com.ming.controller.TestController#patch2021-10-11 11:12:36.286 [restartedMain] INFO com.ming.base.mvc.ControllerAutoRegister- register api :【POST】["/api/test/post"],com.ming.controller.TestController#post
为了偷懒 直接依托于class的 包名 类名 函数名来避免重复 构建api
免得自己去自定义使用
要用自动构建就用指定注解 要用mvc的标准注解 也可以接着使用 互不影响
方便开发
很多地方要判断文件类型
大多数时候 用后缀判断了
后缀判断。。有点自欺欺人
干脆趁着有时间 写个根据文件前4个字节判断文件的实际格式
参考文章:
https://baike.baidu.com/item/%E6%96%87%E4%BB%B6%E5%A4%B4/2695144?fr=aladdin
http://doc.chacuo.net/filehead 常见的文件类型和headerHex对应表
1 | package com.ming.core.utils; |
1 | package com.ming.core.utils; |
一般的文件的头部都是固定的类型 例如 java字节码文件的 咖啡宝贝(CAFEBABE)
这种判断文件类型会比判断后缀靠谱
之前老是用别人的压缩工具类 感觉不太方便 各种各样花式实现
干脆自己完整的了解下 apache commons compressor 工具包 自己封装下
https://commons.apache.org/proper/commons-compress/examples.html
https://www.jianshu.com/p/14af3aeb6db9
这个模块主要功能就是提供归档和压缩功能
归档就是常见的 .zip .tar .jar .7z之类的 可以打包多个文件 可以压缩 也可以不压缩
压缩主要是单纯的压缩数据 例如 .gz .xz .bz2 .lz4 .lzma .sz .zstd 等等
1 | <dependency> |
1 | package com.ming; |
归档: 打开outputStream -> addArchiveEntry -> closeArchiveEntry -> finish
解开归档: 打开inputStream -> getNextEntry -> copy -> write
1 | package com.ming; |
压缩: 获取对应的outputStream -> copy -> flush -> close
解压缩: 获取对应的inputStream -> copy -> write
大致看出 总的来说就是CompressorInputStream解压 CompressorOutputStream压缩 ArchiveInputStream+ArchiveEntry打开归档 ArchiveOuputStream+ArchiveEntry 来打包归档
然后初始化归档或者压缩的inputStream 和outputStream 除了用构造函数外 也提供了CompressorStreamFactory ArchiveStreamFactor 工厂函数
CompressorInputStream构建的时候 可以指定最大允许使用的内存大小 memoryLimitInKb
说到归档+压缩 就不得不说 java io的装饰器设计真的牛叉
1 | package com.ming; |
归档+压缩: 获取CompressorOutputStream -> 把CompressorOutputStream装饰成 ArchiveOutputStream -> addEntry -> finish归档 ->刷新和关闭ArchiveOutputStream和CompressorOutputStream
解压+还原归档: 获取CompressorInputStream -> 把CompressorInputStream装饰成ArchiveInputStream -> 解析还原entry -> 关闭CompressorInputStream和ArchiveInputStream
由于7z过于复杂 commons compressor 是另外实现的 SevenZFile SevenZArchiveEntry SevenZOutputFile
1 | /** |
1 | package com.ming.core.utils; |
提供CompressorUtils测试用例
1 | package com.ming; |
apache commons compressor 提供的常见的压缩和归档算法的实现
在实际使用中 zip 或者 tar配合其他压缩算法用的多 例如 .tar.gz .tar.xz .tar.zstd 等
xz压缩和解压要注意 要引用xz的jar
日常开发中 不仅仅在rpc调用的时候 需要重试啥的
有时候 接第三方系统 也得做一些重试的处理
之前一直都是自己利用function写了一个工具类
现在把自己写的基本思路 和 guava retry 和spring retry 一起记录一下 方便后续自己选择使用
https://github.com/rholder/guava-retrying
https://github.com/spring-projects/spring-retry
https://www.cnblogs.com/jelly12345/p/15292568.html
1 | package com.ming.example.retry; |
1 | public static int FLAG = 0; |
pom配置
1 | <dependency> |
调用方式
1 | public static int FLAG = 0; |
pom依赖
1 | <dependency> |
调用方式
RetryTemplate方式调用
1 | /** |
注解调用方式
1 |
|
自己实现 可以按照对应的需求 实现的更加精细 示例只是个例子 需要自己去额外的处理其他的细节
guava-retrying和spring-retry 大多数使用方式一样 区别较大的地方 就是guava-retrying 支持根据结果去继续重试
没啥好说的 日常复习 想起来juc 发现juc还没记录笔记 每次都是看别人的总结
借着这次机会 对整个juc 做个汇总总结 方便自己速查
java.util.concurrent包,按照功能可以大致划分如下:
全部都是阅读大神的一系列文章 自己记录一下而已
不管是什么类型的锁 总的来说就是保证在部分函数执行的时候保证并发安全的操作 必须要合理的释放锁
公平策略:在多个线程争用锁的情况下,公平策略倾向于将访问权授予等待时间最长的线程。也就是说,相当于有一个线程等待队列,先进入等待队列的线程后续会先获得锁,这样按照“先来后到”的原则,对于每一个等待线程都是公平的。
非公平策略:在多个线程争用锁的情况下,能够最终获得锁的线程是随机的(由底层OS调度)。
1 |
|
ReadWriteLock
也区分公平策略和非公平策略、提供锁重入 和锁降级(只支持写锁降级为读锁)
写锁可以获取 Condition对象 Condition是对wait()和notify()的增强
1 |
|
StampedLock
邮戳锁 在使用的时候返回一个stamp 解锁的时候必须使用对应的stamp解锁
所有获取锁的方法,都返回一个邮戳(Stamp),Stamp为0表示获取失败,其余都表示成功;
所有释放锁的方法,都需要一个邮戳(Stamp),这个Stamp必须是和成功获取锁时得到的Stamp一致
StampedLock是不可重入的;(如果一个线程已经持有了写锁,再去获取写锁的话就会造成死锁)
StampedLock有三种访问模式:
①Reading(读模式):功能和ReentrantReadWriteLock的读锁类似
②Writing(写模式):功能和ReentrantReadWriteLock的写锁类似
③Optimistic reading(乐观读模式):这是一种优化的读模式。
StampedLock可以通过tryConvertToReadLock tryConvertToWriteLock tryConvertToOptimisticRead 三种锁互相转换
无论写锁还是读锁,都不支持Conditon等待
在ReentrantReadWriteLock中,当读锁被使用时,如果有线程尝试获取写锁,该写线程会阻塞。
但是,在Optimistic reading中,即使读线程获取到了读锁,写线程尝试获取写锁也不会阻塞,这相当于对读模式的优化,但是可能会导致数据不一致的问题。所以,当使用Optimistic reading获取到读锁时,必须对获取结果进行校验。
StampedLock为了兼容ReentrantLock、ReadWriteLock 提供了 asReadLock() asWriteLock() asReadWriteLock() 方式转换成对应的xxxView
stampedLock 乐观锁使用固定模式:
1 | // |
1 |
|
AtomicInteger
基础类型的原子封装类 AtomicInteger AtomicBoolean AtomicLong 之类的
jdk8之前 自旋+cas
jdk8之后 都是使用unSafe操作直接cas操作
lazySet+lock 配合使用来减少突破内存屏障的次数 增加性能
1 |
|
AtomicReference
对某个对象的操作实现原子化
以无锁方式访问共享资源的能力 自旋+cas 并且提供记录值的变化次数 或者是否变化的atomic
1 |
|
AtomicIntegerArray
atomicXXXarray 对基本类型的数组提供原子化操作
Unsafe直接操作数组的内存 arraBaseOffset
1 |
|
AtomicReferenceFieldUpdater
也是类似的 有AtomicIntegerFieldUpdater AtomicLongFieldUpdater AtomicReferenceFieldUpdater 各种变体
用法就是对原本不支持原子操作的类中的属性字段 进行包装 在不改变外部调用的情况下 变更为并发安全的操作
1 |
|
LongAdder
利用将数字分组存储 来减少并发热点 提高效率 典型用空间换时间做法
sum求和,这个方法只能得到某个时刻的近似值,这也就是LongAdder并不能完全替代LongAtomic的原因之一
继承Striped64 并且有优化过的并发控制的封装类 基本上原理差不多 主要分两类
xxxAdder 类似currentMap的分段锁的方式实现 把一个类型分开存储到数组中 来减少并发热点 提高效率
xxxAccumulator 增强版xxxAdder 提供自定义计算函数
LongAdder只能针对数值的进行加减运算
1 |
|
名称 | 功能 | 备注 |
---|---|---|
CountDownLatch | 倒数计数器,构造时设定计数值,当计数值归零后,所有阻塞线程恢复执行;其内部实现了AQS框架 | |
CyclicBarrier | 循环栅栏,构造时设定等待线程数,当所有线程都到达栅栏后,栅栏放行;其内部通过ReentrantLock和Condition实现同步 | |
Semaphore | 信号量,类似于“令牌”,用于控制共享资源的访问数量;其内部实现了AQS框架 | |
Exchanger | 交换器,类似于双向栅栏,用于线程之间的配对和数据交换;其内部根据并发情况有“单槽交换”和“多槽交换”之分 | |
Phaser | 多阶段栅栏,相当于CyclicBarrier的升级版,可用于分阶段任务的并发控制执行;其内部比较复杂,支持树形结构,以减少并发带来的竞争 |
CountDownLatch
倒数计数器
基本上两种用法:
1:作为开关or入口
2:作为一个完成某些操作的信号
核心函数:
await():阻塞当前线程 一直到计数器归零
countDown();计数器计数-1
1 |
|
CyclicBarrier
循环栅格 当满足指定数量的参与者 执行一次放行的函数
await()会抛出BrokenBarrierException表示当前的CyclicBarrier已经损坏了,可能等不到所有线程都到达栅栏了,所以已经在等待的线程也没必要再等了,可以散伙了。
出现以下几种情况之一时,当前等待线程会抛出BrokenBarrierException异常:
其它某个正在await等待的线程被中断了
其它某个正在await等待的线程超时了
某个线程重置了CyclicBarrier(调用了reset方法,后面会讲到)
另外,只要正在Barrier上等待的任一线程抛出了异常,那么Barrier就会认为肯定是凑不齐所有线程了,就会将栅栏置为损坏(Broken)状态,并传播BrokenBarrierException给其它所有正在等待(await)的线程。
当一个线程中断了 会造成整个栅栏损坏 给其他未释放的线程发送 BrokenBarrierException
1 |
|
Semaphore
信号量 用来控制稀缺资源使用量的 例如某函数最大并发 等等
acquire() 申请 可以申请多个 申请不到就阻塞 可以使用tryAcquire()
release() 释放 可以归还多个
1 |
|
1 |
|
CountDownLatch:倒数计数器,初始时设定计数器值,线程可以在计数器上等待,当计数器值归0后,所有等待的线程继续执行
CyclicBarrier:循环栅栏,初始时设定参与线程数,当线程到达栅栏后,会等待其它线程的到达,当到达栅栏的总数满足指定数后,所有等待的线程继续执行
Phaser:多阶段栅栏,可以在初始时设定参与线程数,也可以中途注册/注销参与者,当到达的参与者数量满足栅栏设定的数量后,会进行阶段升级(advance)
1 |
|
常用队列比较:
队列特性 | 有界队列 | 近似无界队列 | 无界队列 | 特殊队列 |
---|---|---|---|---|
有锁算法 | ArrayBlockingQueue | LinkedBlockingQueue、LinkedBlockingDeque | / | PriorityBlockingQueue、DelayQueue |
无锁算法 | / | / | LinkedTransferQueue | SynchronousQueue |
五种不同的Node
1 | 1. Node结点 |
常量解释
1 | 基本上和 HashMap差不多 例如链表长度超过8 并且kv数量>64 才会转化treeNode节点 只是treeNode变更为链表的时候 一个是节点<6 还要判断kv数量<16才会转换为链表 |
putVal的四种情况
1 | 1. 首次初始化 -懒加载 |
查询数据的逻辑:
1 | get方法的逻辑很简单,首先根据key的hash值计算映射到table的哪个桶——table[i]。 |
sumCount() 和longAdder 一样 多段汇总
扩容机制:
1 | 扩容思路 |
扩容的原理
1 | CASE1:当前是最后一个迁移任务或出现扩容冲突 |
1 |
|
ConcurrentSkipListMap
跳表map 实现ConcurrentNavigableMap 是一个key有序的map
跳表由很多层组成;
每一层都是一个有序链表;
对于每一层的任意结点,不仅有指向下一个结点的指针,也有指向其下一层的指针。
ConcurrentSkipListMap内部一共定义了3种不同类型的结点,元素的增删改查都从最上层的head指针指向的结点开始:
结点定义:
1 | 普通结点:Node |
1 |
|
ConcurrentSkipListSet
并发控制的跳表顺序set
类似于 treeSet 和NavigableMap treeMap的关系
内部直接使用 ConcurrentSkipListMap使用
ConcurrentSkipListMap对键值对的要求是均不能为null,所以ConcurrentSkipListSet在插入元素的时候,用一个Boolean.TRUE对象(相当于一个值为true的Boolean型对象)作为value,同时putIfAbsent可以保证不会存在相同的Key。
所以,最终跳表中的所有Node结点的Key均不会相同,且值都是Boolean.True。
1 |
|
CopyOnWriteArrayList
写入加锁的arrayList
每次写入或者删除的时候 先获取旧的数组 然后使用 ReentrantLock 直接锁定 保证写入原子性 操作完成之后将新数组替换到原本的读数组
CopyOnWriteArrayList的思想和实现整体上还是比较简单,它适用于处理“读多写少”的并发场景。通过上述对CopyOnWriteArrayList的分析,读者也应该可以发现该类存在的一些问题:
1 |
|
CopyOnWriteArraySet
CopyOnWriteArraySet,从名字上可以看出,也是基于“写时复制”的思想。事实上
CopyOnWriteArraySet内部引用了一个CopyOnWriteArrayList对象,以“组合”方式,
委托CopyOnWriteArrayList对象实现了所有API功能。
1 |
|
ConcurrentLinkedQueue
无锁队列
ConcurrentLinkedQueue底层是基于链表实现的。
Doug Lea在实现ConcurrentLinkedQueue时,并没有利用锁或底层同步原语,而是完全基于自旋+CAS的方式实现了该队列。回想一下AQS,AQS内部的CLH等待队列也是利用了这种方式。
由于是完全基于无锁算法实现的,所以当出现多个线程同时进行修改队列的操作(比如同时入队),很可能出现CAS修改失败的情况,那么失败的线程会进入下一次自旋,再尝试入队操作,直到成功。所以,在并发量适中的情况下,ConcurrentLinkedQueue一般具有较好的性能。
ConcurrentLinkedQueue使用了自旋+CAS的非阻塞算法来保证线程并发访问时的数据一致性。由于队列本身是一种链表结构,所以虽然算法看起来很简单,但其实需要考虑各种并发的情况,实现复杂度较高,并且ConcurrentLinkedQueue不具备实时的数据一致性,实际运用中,队列一般在生产者-消费者的场景下使用得较多,所以ConcurrentLinkedQueue的使用场景并不如阻塞队列那么多。
另外,关于ConcurrentLinkedQueue还有以下需要注意的几点:
ConcurrentLinkedQueue的迭代器是弱一致性的,这在并发容器中是比较普遍的现象,主要是指在一个线程在遍历队列结点而另一个线程尝试对某个队列结点进行修改的话不会抛出ConcurrentModificationException,这也就造成在遍历某个尚未被修改的结点时,在next方法返回时可以看到该结点的修改,但在遍历后再对该结点修改时就看不到这种变化。
size方法需要遍历链表,所以在并发情况下,其结果不一定是准确的,只能供参考。
1 |
|
ConcurrentLinkedDeque
无锁双端队列
提供双端进出的操作的 linkedQueue版本
实现了Deque接口的功能
1 |
|
BlockingQueue
阻塞队列 只是个接口 主要定义实现阻塞类队列的queue 和deque
1 |
|
ArrayBlockingQueue
基于环形数组实现的阻塞队列
内部是一个环形数组
利用takeIndex 和putIndex来表示写入和取出的坐标
内部直接使用ReentrantLock 来处理并发
ArrayBlockingQueue利用了ReentrantLock来保证线程的安全性,针对队列的修改都需要加全局锁。在一般的应用场景下已经足够。对于超高并发的环境,由于生产者-消息者共用一把锁,可能出现性能瓶颈。
另外,由于ArrayBlockingQueue是有界的,且在初始时指定队列大小,所以如果初始时需要限定消息队列的大小,则ArrayBlockingQueue 比较合适。
1 |
|
LinkedBlockingQueue
基于链表实现的阻塞队列
近似无界阻塞队列
如果初始化不指定大小 则默认大小为 Integer.MAX_VALUE
LinkedBlockingQueue除了底层数据结构(单链表)与ArrayBlockingQueue不同外,另外一个特点就是:它维护了两把锁——takeLock和putLock。takeLock用于控制出队的并发,putLock用于入队的并发。这也就意味着,同一时刻,只能只有一个线程能执行入队/出队操作,其余入队/出队线程会被阻塞;但是,入队和出队之间可以并发执行,即同一时刻,可以同时有一个线程进行入队,另一个线程进行出队,这样就可以提升吞吐量。
1 |
|
1 |
|
1 | REQUEST:表示未配对的消费者(当线程进行出队操作时,会创建一个mode值为REQUEST的SNode结点 ) |
整个transfer方法考虑了限时等待的情况,且入队/出队其实都是调用了同一个方法,其主干逻辑就是在一个自旋中完成以下三种情况之一的操作,直到成功,或者被中断或超时取消:
栈为空,或栈顶结点类型与当前入队结点相同。这种情况,调用线程会阻塞;
栈顶结点还未配对成功,且与当前入队结点可以配对。这种情况,直接进行配对操作;
栈顶结点正在配对中。这种情况,直接进行下一个结点的配对。
1 |
|
必须要实现Delayed接口的class才能丢进延时队列
1 |
|
1 |
|
1 |
|
Executors
executors 创建常用的线程池类型
ThreadPoolExecutor在以下两种情况下会执行拒绝策略:
1.当核心线程池满了以后,如果任务队列也满了,首先判断非核心线程池有没满,没有满就创建一个工作线程(归属非核心线程池), 否则就会执行拒绝策略;
2.提交任务时,ThreadPoolExecutor已经关闭了。
拒绝策略:
1 | AbortPolicy(默认):AbortPolicy策略其实就是抛出一个RejectedExecutionException异常: |
通过executors创建不同执行器方式:
1 | //多个线程的执行器 |
1 |
|
ThreadPoolExecutor
普通线程池
线程池的引入,主要解决以下问题:
减少系统因为频繁创建和销毁线程所带来的开销;
自动管理线程,对使用方透明,使其可以专注于任务的构建。
ThreadPoolExecutor内部定义了一个AtomicInteger变量——ctl,通过按位划分的方式,在一个变量中记录线程池状态和工作线程数——低29位保存线程数,高3位保存线程池状态:
ThreadPoolExecutor一共定义了5种线程池状态:
1 | RUNNING : 接受新任务, 且处理已经进入阻塞队列的任务 |
拒绝策略:
1 | AbortPolicy(默认):AbortPolicy策略其实就是抛出一个RejectedExecutionException异常: |
1 |
|
1 | 对Runnable任务进行包装,封装成ScheduledFutureTask,该类任务支持任务的周期执行、延迟执行; |
1 |
|
1 | NEW:表示任务的初始化状态; |
1 |
|
1 | ForkJoinPool:ExecutorService的实现类,负责工作线程的管理、任务队列的维护,以及控制整个任务调度流程; |
3类外部提交任务的方法:invoke、execute、submit,它们的主要区别在于任务的执行方式上。
1 | 通过invoke方法提交的任务,调用线程直到任务执行完成才会返回,也就是说这是一个同步方法,且有返回结果; |
ForkJoinPool对象的构建有两种方式:
ForkJoinTask实现了Future接口,是一个异步任务,我们在使用Fork/Join框架时,一般需要使用线程池来调度任务,线程池内部调度的其实都是ForkJoinTask任务(即使提交的是一个Runnable或Callable任务,也会被适配成ForkJoinTask)。
除了ForkJoinTask,Fork/Join框架还提供了两个它的抽象实现,我们在自定义ForkJoin任务时,一般继承这两个类:
RecursiveAction:表示没有返回结果的ForkJoin任务
RecursiveTask:表示具有返回结果的ForkJoin任务
调用task的fork()方法会ForkJoinPool.commonPool()方法创建线程池,然后将自己作为任务提交给线程池。
ForkJoinWorkerThread
Fork/Join框架中,每个工作线程(Worker)都有一个自己的任务队列(WorkerQueue), 所以需要对一般的Thread做些特性化处理,J.U.C提供了ForkJoinWorkerThread类作为ForkJoinPool中的工作线程
ForkJoinWorkerThread 在构造过程中,会保存所属线程池信息和与自己绑定的任务队列信息。同时,它会通过ForkJoinPool的registerWorker方法将自己注册到线程池中。
线程池中的每个工作线程(ForkJoinWorkerThread)都有一个自己的任务队列(WorkQueue),工作线程优先处理自身队列中的任务(LIFO或FIFO顺序,由线程池构造时的参数 mode 决定),自身队列为空时,以FIFO的顺序随机窃取其它队列中的任务。
WorkQueue
任务队列(WorkQueue)是ForkJoinPool与其它线程池区别最大的地方,在ForkJoinPool内部,维护着一个WorkQueue[]数组,它会在外部首次提交任务)时进行初始化:
volatile WorkQueue[] workQueues; // main registry
当通过线程池的外部方法(submit、invoke、execute)提交任务时,如果WorkQueue[]没有初始化,则会进行初始化;然后根据数组大小和线程随机数(ThreadLocalRandom.probe)等信息,计算出任务队列所在的数组索引(这个索引一定是偶数),如果索引处没有任务队列,则初始化一个,再将任务入队。也就是说,通过外部方法提交的任务一定是在偶数队列,没有绑定工作线程。
WorkQueue作为ForkJoinPool的内部类,表示一个双端队列。双端队列既可以作为栈使用(LIFO),也可以作为队列使用(FIFO)。ForkJoinPool的“工作窃取”正是利用了这个特点,当工作线程从自己的队列中获取任务时,默认总是以栈操作(LIFO)的方式从栈顶取任务;当工作线程尝试窃取其它任务队列中的任务时,则是FIFO的方式。
我们在ForkJoinPool一节中曾讲过,可以指定线程池的同步/异步模式(mode参数),其作用就在于此。同步模式就是“栈操作”,异步模式就是“队列操作”,影响的就是工作线程从自己队列中取任务的方式。
ForkJoinPool中的工作队列可以分为两类:
有工作线程(Worker)绑定的任务队列:数组下标始终是奇数,称为task queue,该队列中的任务均由工作线程调用产生(工作线程调用FutureTask.fork方法);
没有工作线程(Worker)绑定的任务队列:数组下标始终是偶数,称为submissions queue,该队列中的任务全部由其它线程提交(也就是非工作线程调用execute/submit/invoke或者FutureTask.fork方法)。
F/J框架的核心来自于它的工作窃取及调度策略,可以总结为以下几点:
1 | 每个Worker线程利用它自己的任务队列维护可执行任务; |
任务提交
任务提交是整个调度流程的第一步,F/J框架所调度的任务来源有两种:*
①外部提交任务
所谓外部提交任务,是指通过ForkJoinPool的execute/submit/invoke方法提交的任务,或者非工作线程(ForkJoinWorkerThread)直接调用ForkJoinTask的fork/invoke方法提交的任务:
clipboard.png
外部提交的任务的特点就是调用线程是非工作线程。这个过程涉及以下方法:
ForkJoinPool.submit
ForkJoinPool.invoke
ForkJoinPool.execute
ForkJoinTask.fork
ForkJoinTask.invoke
ForkJoinPool.externalPush
ForkJoinPool.externalSubmit
②工作线程fork任务
所谓工作线程fork任务,是指由ForkJoinPool所维护的工作线程(ForkJoinWorkerThread)从自身任务队列中获取任务(或从其它任务队列窃取),然后执行任务。
工作线程fork任务的特点就是调用线程是工作线程。这个过程涉及以下方法:
ForkJoinTask.doExec
WorkQueue.push
创建工作线程
任务提交完成后,ForkJoinPool会根据情况创建或唤醒工作线程,以便执行任务。
ForkJoinPool并不会为每个任务都创建工作线程,而是根据实际情况(构造线程池时的参数)确定是唤醒已有空闲工作线程,还是新建工作线程。这个过程还是涉及任务队列的绑定、工作线程的注销等过程:
ForkJoinPool.signalWork
ForkJoinPool.tryAddWorker
ForkJoinPool.createWorker
ForkJoinWorkerThread.registerWorker
ForkJoinPool.deregisterWorker
任务执行
任务入队后,由工作线程开始执行,这个过程涉及任务窃取、工作线程等待等过程:
ForkJoinWorkerThread.run
ForkJoinPool.runWorker
ForkJoinPool.scan
ForkJoinPool.runTask
ForkJoinTask.doExec
ForkJoinPool.execLocalTasks
ForkJoinPool.awaitWork
任务结果获取
任务结果一般通过ForkJoinTask的join方法获得,其主要流程如下图:
任务结果获取的核心涉及两点:
互助窃取:ForkJoinPool.helpStealer
算力补偿:ForkJoinPool.tryCompensate
1 |
|
juc 没啥好说的 必须要了解和学习的
而且并发处理和分布式处理几乎一模一样 很多理论工具互通
有时候小项目 直接上ci cd真心划不来
写shell脚本 有不是所有的大哥都是linux mac下开发
然后就找了下插件 找到这个 wagon-maven 插件
这个插件主要功能就是上传下载、远程服务器执行某些命令
插件很简单
官网:http://www.mojohaus.org/wagon-maven-plugin/
maven中央仓库地址: https://mvnrepository.com/artifact/org.codehaus.mojo/wagon-maven-plugin
指定一个固定部署相关文件的目录 使用插件的上传目录的方式 去上传并且执行相关命令
使用settings.xml 配置需要连接的服务器的信息
配置一个id 为 ming-ubuntu的服务器配置
1 | <servers> |
在root目录编写执行脚本 deploy.sh 内容如下:
1 | !/bin/bash |
1 | <build> |
1 | mvn -DskipTests clean install package |
就是一个小工具
直接shell脚本实现 也很简单 但是这样兼容性会更好点
不过要注意 shell脚本的编码格式 是LF(linux/mac) 还是 CRLF(windows) 一定要是 LF否则在linux上执行会无法识别
druid连接池 没啥好说的 的确好用 虽然极限性能可能跟 hikari低点 但是功能多啊
各种基本sql监控 、扩展等等
https://github.com/alibaba/druid
https://github.com/alibaba/druid/wiki/%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98
例子在spring boot项目中引用
1 | <druid.starter.version>1.2.8</druid.starter.version> |
web ui地址 http://xxx.xx/spring-boot-druid
1 | spring: |
1 | package com.ming.base.mvc; |
FilterEventAdapter 扩展了一些前置后置操作的函数 一般都继承这个来实现filter
1 | package com.ming.core.orm; |
在META-INF目录下增加 druid-filter.properties
如果是maven管理 直接在resources下增加META-INF/druid-filter.properties
1 | # 此处指定一个druid的filter名称为 my class地址为com.ming.core.orm.JdbcConnectionFilter |
1 | spring.datasource.druid.filters=my |
druid 好用 官方文档也写的很清楚
这里只是作为速查使用
spring boot 默认内嵌是tomcat
如果只是想最简单提升一下性能 可以把tomcat 换成 undertow
至于哪里强 可以看看一些web服务器对比 undertow还是很能打的
1 | <spring-boot.version>2.5.6</spring-boot.version> |
1 | server: |
1 | package com.ming.base.config; |
1 | package com.ming; |
1 | 。。。 |
undertow jboss出品
没啥毛病
对象属性转换 方式有很多
例如 各种beanUtils或者dozer 但是mapstruct 是类似lombok一样 在编译器直接生成性能最高的 直接调用set的方式
在编译器能够提示大多数的映射异常
1 | <lombok.version>1.18.20</lombok.version> |
1 | package com.ming.mapstuct; |
1 | package com.ming.mapstuct; |
此代码为mapper编译后生成的 用idea反编译出来看的
可以清晰的看到 按照mapping定义的方式 编译成对应的代码
1 | // |
1 | package com.ming.mapstuct; |
1 | demo11:Demo11(name=ming, age=200, createTime=2022-02-11T17:57:09.729450200) |
其实使用cglib的BeanCopier 性能也不低 不过一些特殊情况就不好处理
mapstruct 定义比较清晰 而且编译期间可以提示大多数情况的错误
就是这玩意跟lombok一样 要影响编译
如果项目使用了静态增强的aop 配置起来会比较麻烦
最近遇到需要用java 把markdown解析成html
这个东西 很多工具 比较出名的 有commonmark-java 和他的衍生版本 flexmark-java
从性能上来说 肯定是commonmark-java最快 所以我也选择用这个
官网地址:
https://github.com/commonmark/commonmark-java
https://github.com/vsch/flexmark-java
1 | <commonmark.version>0.18.1</commonmark.version> |
1 | package com.ming; |
主要就是增加按照不同的类型添加节点的属性的实现 方便美化生成的html
1 | package com.ming.service; |
解析工具 原理也就是解析markdown的规则 生成文本
现成的工具 没啥好说的 作为速查
记录一下redis-module的安装和使用
示例在ubuntu20.04版本上操作
1 | sudo add-apt-repository ppa:redislabs/redis |
1 | # 配置监听的ip |
1 | 启动 |
1 | 编辑 redis.conf 永久加载模块 |
源码安装redisearch模块作为示例
https://www.cnblogs.com/lina-2159/p/14335919.html
打包编译模块
1 | git clone https://github.com/RediSearch/RediSearch.git |
配置加载模块
1 | 编辑/etc/redis/redis.conf |
在redis console上操作
1 | 创建索引并且定义格式 |
在ubuntu 20.04上安装了一下redisearch模块
安装还是很方便的 有了module这个功能 各种常用的操作都可以借助redis module实现了
例如 全文检索、图数据库、限流器、布隆过滤器、分布式锁、等等 具体的可以看看 官网的模块