基于c语言开发高性能key-value存储非关系形数据库数据库。
一 基础知识
1.1 五种类型操作
1.1.1 String
1. 脚本操作:
# 添加
set key value
# 获取
get key
# 删除数据
del key
# 添加或者修改多个数据
mset key1 value1 key2 value2
# 获取多个数据
mget key1 key2
1.1.2 hash
每一个key对应的value,类似HashMap集合存储数据的结构。底层使用哈希表结构实现数据存储。
1. 脚本操作:
# 添加或者修改数据
hset key field value
# 获取数据
hget key fileld
hgetall key
# 删除数据
hdel key field1 [field2]
# 添加或者修改数据
hmset key field1 value1 field2 value2
#获取多个数据
hmget key field1 field2
1.1.3 List
存储一个key对应多个数据,并且数据的存入和取出顺序是一致的。并且底层使用双向列表。
1. 脚本操作:
# 添加/修改数据
lpush key value1 [value2] ......
rpush key value1 [value2] ......
# 获取数据
lrange key start stop
lindex key index
# 获取并移除数据
lpop key
rpop key
1.1.4 Set
一个key对应多个value值,不能存储重复元素。存储大量数据,查询效率更高。
1. 脚本操作:
# 添加数据
sadd key value1 [value2]
# 获取全部数据
smembers key
# 删除数据
srem key member1 [member2]
1.2 key
1.2.1 key基本操作
key是一个字符串,在redis中通过key能获取值。
1. 脚本操作:
# 删除指定key
del key
# 获取key是否存在
exists key
# 获取key类型
type key
# 设置key有效时间
expire key seconds
pexpire key milliseconds
# 获取key有效时间
ttl key
pttl key
二 持久化
reids中数据存储在内存中。如果redis一直运行,则数据会一直保存在内存中,我们随时都可以读取。但是在现实中,redis可能可能死机,或者部署redis服务器崩溃了。需要重启服务器或者redis,redis原先内存中数据就会丢。所以为了保证redis中数据的安全性,redis就设计了持久化。redis存储数据时候,会同时将数据存储到硬盘上。如果重启redis,将硬盘数据恢复到redis中。Redis持久化到硬盘中两种方式,RDB(日志指令)和AOF(数据快照)。
2.1 持久化基本概述
1. 什么是持久化:
将内存种数据存储到内盘中等永久存储,在一定的时机再从新恢复数据。
2. 持久化两种方式:
计算机中数据是二进制存储,将这二进制数据原封不动的记录下来,也叫快照存储(RDB),保存的是某一时刻数据。
将改变数据的操作命令保存下来,即保存操作过程,称为日志(AOF)。
2.2 RDB(快照)
2.2.1 save指令
1. 手动执行save指令:
save
2. save指令相关配置:
#配置本地数据库文件名,默认dump.rdb,常设置为dump-端口号.rdb
dbfilename filename
# 设置存储文件名
dir path
# 存储到本地数据库,是否压缩
rdbcompression yes|no
# 在读写过程是否对RDB格式校验,节约10%的时间消耗
rdbchecksum yes|no
备注:
save指令会阻塞当前Redis服务器,知道RDB过程完成位置,会造成长时间阻塞。不建议使用
2.2.2 bgsave
1. 手动执行bgsave指令:
bgsave
2. bgsave指令相关配置:
# 后台出现错误,是否停止保存,默认yes
stop-writes-on-bgsave-error yes|no
dbfilename filename
dir path
rdbcompression yes|no
rdbchecksum yes|no
3. 配置bgsave自动执行:
监控时间key变化量
save second changes
例如:
# 900秒,有一个key发生变化
save 900 1
# 300秒,有10个key发生变化
save 300 10
# 60秒,有10000个key发生变化
save 60 10000
完整配置
save second changes
filename filename
dir path
rdbcompression yes|no
rdbchecksum yes|no
stop-writes-on-bgsave-error yes|no
4. bgsave执行原理:
针对save命令进行优化,Redis中所有涉及RDB操作都采用bgsava方式。
当客户端给服务端发送一个save指令时候,服务端立马给客户端返回一个结果,同时创建一个子线程执行save操作。
2.2.3 RDB总结
优点:
RDB时一个二进制文件,存储效率很高,比AOF速度快很多。
缺点:
无法做到实时持久化,容易造成数据丢失。
因为bgsave都会创建一个子线程,会牺牲一些性能。
Redis众多版本中,RDB的二进制文件无法统一,各个版本的服务之间数据格式无法兼容。
2.3 AOF(日志)
以独立日志方式,记录每次读写命令。重启服务时,执行AOF文件中命令。主要用于解决数据实时性,是Redis持久化的主流方式。
2.3.1 AOF日志配置
# 开启AOF持久化功能
appendonly yes|no
# AOF持久化名字,默认名字为appendonly.aof
appendfilename filename
# AOF保存路径,和RDB路径保持一致即可
dir path
# AOF写数据策略,默认everysec
appendfsync always|everysec|no
备注(AOF写三种策略):
always(每次):每次写操作都会同步到AOF文件中,性能低。
everysec(每秒):每秒将缓冲区命令同步到AOF文件中,系统在突然宕机情况会有一秒钟数据丢失,数据准确性较高,性能高,建议使用。
no(系统控制):操作系统控制每次同步到AOF文件周期。
2.3.2 AOF重写
对同一数据若干指令转化为最终结果的对应指令。
1. 重写的作用:
将多条命令压缩成一条。降低磁盘占用率,提高恢复效率。
2. 重写的规则:
进程中有时效数据,并且已经超时,不写进AOF文件中。
对于无效指令,直接忽略。
对一条数据的多条写命令合并成一条命令。
3. 重写配置:
手动执行
bgrewriteaof
自动重写配置
auto-aof-rewrite-min-size size
auto-aof-rewrite-percentage percentage
4. AOF写和重写流程:
2.4 RDB和AOF应用场景
1. 对数据十分敏感,建议使用AOF持久化方案:
AOF持久化策略使用everysecond,默认一秒钟同步一次。该策略redis仍然能保持很好的性能,
当机器突然出现故障,也就丢失0-1秒钟数据。
2. 数据呈现阶段有效,使用RDB持久方案:
良好的保持数据阶段不丢失,且恢复时间快。
三 数据删除与淘汰策略
3.1 过期数据删除(key已经过期)
3.1.1 数据状态
内存中数据通过TTL指令获取状态
正数: 数据在内存中有存活时间。
-1:永久存在。
2:已过期数据,或者被删除的数据。
3.1.2 Redis中时效性数据存储结构
过期数据是一块独立的存储空间,Hash结构。field是value的内存地址,value是过期时间。最终进行过期处理时候,对该空间数据进行检测,当时间到期后通过filel找到数据地址,进行相关操作。
3.1.3 数据删除策略(过期数据)
在内存占用和cpu占用之间寻找一种平衡,不能顾此失彼造成redis性能下降,引发服务器宕机和内存泄漏。针对过期数据删除策略如下:
定时删除
惰性删除
定期删除
1. 定时删除:
创建一个定时器,key设置过期时间,当到达过期时候,定时器任务立即执行对键删除。
总结:
到时就删除,节约内存。但是造成cpu压力大,影响服务器响应时间和指令吞吐量。即拿时间换空间。
2. 惰性删除:
数据到达过期时间,不做处理。下次访问该数据时进行删除。
内存压力大,长期占用内存空间。但是节省CPU性能,发现时删除。即拿时间换空间。
3. 定期删除:
相对前两种方案的一种折中方案。
删除过程:
Redis启动服务器初始化时,读取配置server.hz的值,默认为10.
每秒执行server.hz次serverCron()-------->databasesCron()--------->activeExpireCycle()
activeExpireCycle()对每个expires[]进行逐一监测。
对某个expires[]检测时,随机挑选W个key检测
如果key超时,删除key。
如果一轮中删除的key的数量>W*25%,循环该过程。
如果一轮删除的key的数量<W*25%,检查下一个expires[*]
W取值=ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP属性值
总结:
周期性轮询redis库中时效性数据,采用随机抽取数据,利用过期的比例来控制删除频率。
检测频率可以自定义设置,内存压力不是很大,长期占用内存的冷数据会被持续的删除。即随机抽查重点抽查。
3.2 数据淘汰(redis中内存不足)
在redis中执行命令之前会调用freeMemoryIfNeeded()检查内存是否充足。如内存不满足,则要删除一些数据。清除数据策略称为逐出算法。
3.2.1 策略配置
# 使用最大内存,生成环境上设置为50%以上
maxmemory ?mb
# 每次选取待删除数据个数
maxmemory-samples count
# 对数据进行删除,选择策略
maxmemory-policy policy
删除数据策略(三类八种)
第一类:检测易失数据
volatile-lru:挑选最近最少使用的数据淘汰
volatile-lfu:挑选最近使用次数最少的数据淘汰
volatile-ttl:挑选将要过期的数据淘汰
volatile-random:任意选择数据淘汰
第二类:检测全库数据
allkeys-lru:挑选最近最少使用的数据淘汰
allkeLyRs-lfu::挑选最近使用次数最少的数据淘汰
allkeys-random:任意选择数据淘汰,相当于随机
第三类:放弃数据驱逐
no-enviction(驱逐):禁止驱逐数据(redis4.0中默认策略),会引发OOM(Out Of Memory)
三 原理分析
3.1 基础概念
1. Redis是单线程还是多线程架构:
redis整体并非是一个线程,redis在处理网络请求和k/v读写操作时候是一个线程。而持久化,异步删除,集群数据同步都是额外线程进行处理。
2. 单线程为啥这么快?:
redis大部分操作是基于内存中。
因为只有一个线程,避免多线程上下文切换和竞争。
redis底层采用IO多路复用技术,大量高并发下,提高系统吞吐量。
3.2 多路复用
3.2.1 前置知识
1. file descriptor(fd):
文件描述符。(Linux中一些皆文件,比如:普通文件、目录文件、连接文件、设备文件等。)文件描述符是内核为了高效管理系统打开文件而产生的一个索引(指针),是一个非负数,所有的io操作的系统调用都是通过fd来操作。
2. 内核空间和用户空间:
内核能管理系统所有资源,磁盘读写,网络IO读写,内存分配回收、进程管理等;他能访问受保护资源,能访问底层硬件设备。应用程序想要操作底层硬件必须通过内核来进行访问。
3.2.2 epoll(IO多路复用分析)
3.2.2.1 阻塞型通信分析
阻塞网络IO流程图:
a 启动TestSocket服务->代码执行流程如下:
b ServerSocket ss = new ServerSocket(8888);--->
内核调用socket()方法,返回一个文件描述符3。
调用bind(3,8888)方法,将fd为3绑定8888端口。
调用listen(3,50),创建socket监听对象,fd为3监听8888端口。
c Socket s = ss.accept();
调用系统函数accept(3,...) = 5,服务端应用等待着内核响应,内核等待着客户端连接,所以应用处理阻塞中。
当有客户端连接创建一个socket连接对象。
d 获取客户端发过来数据。
调用系统函数recv(5,...) recvfrom(5,...) recvmsg(5,...)函数,内核等到客户端的数据发送。应用等待内核响应。
内核接受到数据,返回给应用。
e 将数据返回到客户端。
应用调用send(5,...)等系统函数,将数据发给内核。内核发送完毕返回给应用。
总结:
在服务端接受客户端数据时候,因为应用通过内核调用系统函数accept(3,...) 、recv(5,...)、send(5,...),必须等到内核响应后,应用才能进行下一步操作。所以等待连接、等待发送数据、发送数据都是阻塞的。
因为应用是一个主线程执行,所以当有多个客户端进行连接。必须等待上一个线程完全执行完主线程,才能进行下一个线程处理。
3.2.2.2 非阻塞网络传输分析
异步通信和上面同步通信流程类似。只不过在调用系统函数accept(3,...) 、recv(5,...)、send(5,...)等前,都调用系统函数fcntl(5,....)
导致应用调用accept(3,...) 、recv(5,...)、send(5,...)立马执行下一步,不用等待内核处理完成响应数据。从而导致程序异步。提高
网络通信的性能。
但是因为应用不等待内核处理完毕,所以应用要不断的轮询向内核获取数据。
3.2.2.3 一个线程处理多个客户端连接
linux中提供了select/poll/epoll系统函数支持这一模型。它支持应用程序将一个或者多个fd交给内核,内核检测fd上的状态变化(连接、读、写等)
流程分析
a 应用程序调用epoll_create(1024)函数返回fd为5。
创建epoll对象,创建监听树,创建队列
socket函数,创建一个fd6
bind函数,将fd6进行绑定
listen将fd6监听
fcntl函数让epoll_ctl变成异步。
b 调用epoll_ctl函数
创建一个socket节点到监听树上(注册了节点的监听事件)。
c epoll_wait 函数
轮询的从队列中取出,然后处理多个事件。保证了一个线程处理多个客户端连接。
总结:
epoll模型优势:
没有fd模型限制,不会随着fd增加导致IO效率降低。
不需要每次将fd拷贝到内核空间,只需要一次拷贝,后面复用。
mmap技术加速了用户空间和内核空间数据访问。
3.2.3 Select(IO多路复用)
总结:
select支持跨平台的。但是需要自己维护fd_set,每次都需要fdSet从用户空间拷贝到内核空间。如果fd较多则是一个耗时操作。如果fdset中只有少数的fd在活跃,性能不高。
3.2.4 高性能的Redis有哪些慢操作?什么样操作影响性能?
四 Redis高可用之-主从复制
当redis服务不可用时候,导致应用服务不可用。物理故障可能导致数据丢失。redis提供了主从复制功能。
4.1 Redis主从复制初认识
4.1.1 主从复制优点和弊端
1. 优点:
如果主节点宕机后,从节点作为主节点备份顶上来。扩展了主节点读能力。
2. 缺点:
a master宕机之后,需要从slave选出一个master。然后其他的slave需要复制新master数据。同时还需要通知客户端新的master数据。这个过程就叫做故障转移,但是限制这个过程仍然需要人工参与。
b redis的写能力依然受到单机的限制。
c redis存储能力也受单机的限制。
4.1.2 主从复制流程
1. 建立连接:
2. 数据同步:
数据同步注意问题:
a master数据量大,数据同步应该避免高峰。
b 复制缓冲区大小要合理,会导致数据溢出。或者复制周期长,复制部分数据,发现数据丢失,进行第二次全量复制,slave陷入死循环。
c master主机内存不要太大,占用百分之50-70。留百分之30-50用于执行bgsave和创建复制缓冲区。
3. 命令转移:
master库数据状态被修改后,导致主从服务器状态不一致,需要让主从同步到一致的状态。同步叫命令传播。
复制缓冲器:
是一个队列,用于存储服务器执行命令。每次传播命令,master都会将命令记录下来,并存储到复制缓冲区中。
总结:
[站外图片上传中...(image-ac6d25-1623382960682)]
4.2 哨兵架构
redis节点出现故障时,sentinel能够自动完成故障发现和转移,并通知应用端,实现真正高可用。
4.2.1 哨兵架构分析
1. 哨兵架构做了那些事情:
监控: 每个哨兵节点会定时检测redis数据节点以及其他sentinel节点是否可达。
主节点故障转移: 当master出现故障时候,从slave节点中选取一个master,从slave复制新的master的数据。并且维持后续主从关系。
通知: 哨兵在完成故障转移后,会通知连接客户端故障转移的结果。
配置提供者: 哨兵架构中应用端配置了sentinel的节点集合,通过sentinel获取master信息。
2. sentinel配置多个节点好处?
对主节点进行故障判断是由所有的sentinel共同判断,防止误判。
sentinel有多个,即使个别的sentinel不可用,但整体还是可以用的。
4.2.2 sentinel三个定时任务
1. 第一个定时任务:
每隔10秒,每个sentinel节点向master和slave发送info命令,作用如下:
a 向主节点发送info命令可用获取从节点信息,(sentinel无需配置从节点),当有新的从节点加入,立刻感应维护正确的拓扑结构。
b 根据info命令的回复动态更新sentinel中维护主从节点的完整信息。
2. 第二个定时任务:
[站外图片上传中...(image-3be466-1623382960682)]
每隔2s,每个sentinel节点向sentinel:hello节点发布当前sentinel信息和sentinel对主节点判断,同时其他sentinel节点订阅该频道,作用如下:
a 发现其他sentinel节点。
b sentinel节点交换主节点状态,作为客观下线和领导者选举的依据。
3. 第三个定时任务:
[站外图片上传中...(image-7ae324-1623382960682)]
每隔一秒钟每个sentinel要向其他sentinel节点和redis节点发送ping。判断这些节点是否可达,从而检查节点健康状况。
4.2.3 主观下线和客观下线
[站外图片上传中...(image-39944e-1623382960682)]
主观下线是某一个sentinel一家之言,存在误判操作。
如果是从节点或者其他sentinel节点主观下线,没有后续操作。如果是主节点还需进行客观下线判断。
4.2.4 故障转移过程
- 没有足够的sentinel节点同意主节点下线,主节点主观下线被移除。当主节点从新向sentinel发送ping有效回应时,主节点主观下线移除。
- 主节点下线后,sentinel向主节点和所有从节点发送info命令,由之前10s一次变为每秒一次。
- 接下来进行故障转移前选举出sentinel的leader
因为故障转移工作仅仅是需要一个sentinel节点完成。所有sentinel节点之间要进行选举,选出一个leader完成故障转移。
每一个sentinel都有可能成为leader。
每个sentinel只有一张票,只能投给sentinel,先到先得。
如果某个sentinel得票数>=一半,选举成功。如果选举失败,进行下一轮选择。
[站外图片上传中...(image-e38a21-1623382960682)]
- sentinel的leader节点完成故障转移
从salve节点中选举一个从节点,并升级为主节点。
从节点从新复制主节点数据。
已下线主节点变成从节点,并复制新的主节点数据。(这个设置由于主节点已经下线,无法立刻通知。只能将该设置放到sentinel中,当下线主节点上线,sentinel会将设置发送)
将故障转移结果告诉其他sentinel。(sentinel-leader节点将主节点相关信息,通过发布订阅方式完成)
[站外图片上传中...(image-721261-1623382960682)]
- sentinel将故障转移结果通知客户端。
[站外图片上传中...(image-62f77d-1623382960682)]
sentinel会在相关频道发布故障转移相关信息,应用端只需要去自己感兴趣频道订阅即可。
a 根据sentinel节点 调用sentinelGetMasteAddrByName获取master相关信息。
b 为每一个sentinel创建一个监听线程,并订阅“+switch-master”的频道。(sentinel完成故障转移后会在“+switch-master”频道发布新的master信息)
c 客户端从新初始化,连接master。
五 Redis高可用之-集群
虽然主从复制和哨兵架构能解决高可用问题。但是无法扩展写能力和存储能力。大数据量和高并发无法满足高可用。redis提供了分布式解决方案。
5.1 分布式解决方案
1. 客户端分区:
[站外图片上传中...(image-468e5a-1623382960682)]
2. 代理分区:
[站外图片上传中...(image-70cb1c-1623382960682)]
3. 查询路由分区:
[站外图片上传中...(image-dbafd7-1623382960682)]
5.2 redis分区理论
cluster采用虚拟槽分区方案。redis定义了16384个槽,编号0-16383。每个master属于16384哈希槽中一部分。
执行GET/SET/DEL根据key进行操作,Redis通过CRC16算法计算key,得到redis节点。然后操作指定的redis节点。
[站外图片上传中...(image-f83d7a-1623382960682)]
redis扩容:
新增的redis节点中没有卡槽分配,因此需要从新分配卡槽,还需要考虑redis中数据迁移。
配置文件:
./redis-cli --cluster reshard 192.168.211.141:7001 --cluster-from c9687b2ebec8b99ee14fcbb885b5c3439c58827f,80a69bb8af3737bce2913b2952b4456430a89eb3,612e4af8ea e48426938ce65d12a7d7376b0b37e3 --cluster-to 443096af2ff8c1e89f1160faed4f6a02235822a7 -cluster-slots 100
#参数说明
--cluster-from:表示slot目前所在的节点的node ID,多个ID用逗号分隔
--cluster-to:表示需要新分配节点的node ID --cluster-slots:分配的slot数量
1. Cluster请求路由:
[站外图片上传中...(image-d2345f-1623382960682)]
六 灾难解决
6.1 缓存穿透
用户查询缓存没有查到数据,然后查询MySQL数据库也没有查询到数据,然后用户反复刷新导致反复查询数据库,这种现象叫做缓存穿透。
1. 解决方案一:
第一次查询查询缓存和数据库都没查到数据,此时将null做为value存储到缓存中。下次同样的请求就直接从缓存中取出null。
[站外图片上传中...(image-cf4221-1623382960682)]
2. 第二种解决方案(布隆过滤器):
布隆过滤器是什么?:
用于解决大规模数据情况下不需要精准过滤场景。
布隆过滤器内部是一个bit数组,以及2个hash函数((f_1,f_2))。布隆过滤器有个误判率概念,误判率越高,数组越短,误判率越低,数组越长。
如果有两个数字,N_1经过函数f_1,f_2计算出两个数字,让存储到bit数组中。N_2经过f_1 f_2计算也产生两个数字。当两个数字和和N_1产生的数字有一个一样,则代表N_2在集合中,这就是布隆过滤器的计算原理。
[站外图片上传中...(image-1c22ae-1623382960682)]
解决思路:
在查询缓存时候,先去缓存布隆器中查询。如果在缓存布隆器中查询到结果后,则进行缓存查询。如果没有查询到直接返回,不进行查询。
6.2 缓存击穿
缓存过期,正在此时大量的请求访问某个key,大量请求查询数据库,这种现象叫做缓存击穿。
定时器:
后台定义一个定时器,定时主动更新缓存。例如:某个在缓存中的数据,在一分钟过期,我每隔30秒去更新下缓存数据。
这种方案思路简单,但是增加系统的复杂性。对于key相对固定的,适合。
多级缓存:
[站外图片上传中...(image-6a1ae3-1623382960683)]
我们应用程序将数据存储到缓存中,并设置永不过期。用户进行查询的时候,先查询ngnix缓存,缓存不存在,查询redis缓存中,并将数据存储到Nginx一级缓存,并设置更新时间。不仅防止缓存击穿,还提升程序抗压能力。
分布式锁(解决超卖):
和锁、同步代码块实现功能是一样的,只是使用的业务场景不一样。普通锁和同步代码块只能解决单体服务的,分布式锁解决分布式集群环境。
使用Redission实现分布式锁:
1. 引入依赖包。
2. 创建redis集群配置文件,添加配置。
3. 定义获取锁、释放锁方法。
4. 创建Redisson工厂。
[站外图片上传中...(image-7174ec-1623382960683)]
队列术:
面对封流时候,可以直接将流量放到队列中。让后台不用同时处理更多请求,让队列中请求逐个消费。
Nginx缓存队列术:
了Nginx的代 理缓存,其中有一个属性叫 proxy_cache_lock。多个客户端请求一个缓存中不存在的文件。只允许第一个请求发送到服务端,其他请求在缓存中取到取到信息。
6.3 雪崩
大量的缓存失效,导致大量请求查询数据库,这种现象叫做雪崩。
解决方案:
多级缓存
限流
队列限流
数据预热
6.4 缓存一致性
数据库中数据发生了更改,需要更新缓存中的数据。解决方案canal。
6.4.1 缓存一致性的原理
[站外图片上传中...(image-ecd675-1623382960683)]
使用Canal监听数据库指定表的增量变化,在Java程序中消费Canal监听到的增量变化,并在Java程序中实现对Redis和Nginx缓存更新。
1. Mysql主从复制原理:
a 将mysql master数据变更记录到二进制文件中。
b MySQL slave将master二进制文件拷贝到自己中继日志中。
c MySQL slave从放relay log日志,同步变更数据。
2. Canal工作原理:
a Canal伪装成slave,向master发送drump协议。
b master接受命令之后,向slave(Canal)发送binary log。
c Canal开始解析binary log。
6.4.2 认识Canal
Canal用于基于Mysql增量日志解析,并提供增量数据订阅和消费。
1. Canal应用场景:
搜索引擎和缓存的更新。
代替轮询方式来对数据库表变更进行监控,有效缓解轮询导致数据库资源。
6.4.3 Canal的配置
1. 开启MySQL的bin-log日志:
cd /etc/mysql/mysql.conf.d
在mysqld.cnf下面添加如下配置
# 开启
binlog log-bin=/var/lib/mysql/mysql-bin
# 选择 ROW 模式
binlog-format=ROW
# 配置 MySQL replaction 需要定义,不要和 canal 的 slaveId 重复
server-id=12345
2. Canal安装:
docker run -p 11111:11111 --name canal -d docker.io/canal/canal-server
3. 配置CanalServer:
配置Canal的id:
/home/admin/canal-server/conf/canal.properties
[站外图片上传中...(image-906a36-1623382960683)]
配置数据库监听地址和监听数据库以及表变化:
/home/admin/canal-server/conf/example/instance.properties
[站外图片上传中...(image-16f8ce-1623382960683)]
配置regex规则
a 多个正则用逗号隔开(,),转义符要双斜杠(\\)
b 所有表:.* or .*\\..*
c canal schema下所有表: canal\\..*
d canal下的以canal打头的表:canal\\.canal.*
e canal schema下的一张表:canal.test1
f 多个规则组合使用:canal\\..*,mysql.test1,mysql.test2 (逗号分隔)
重启canal:
docker restart canal
MySQL创建账号并授权:
create user canal@'%' IDENTIFIED by 'canal'; GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT,SUPER ON *.* TO 'canal'@'%'; FLUSH PRIVILEGES;
6.4.4 同步更新缓存
1. 创建类MoneyLogSync,继承EntryHandler类,实现方法:
@CanalTable(value = "money_log")
@Component
public class MoneyLogSync implements EntryHandler<MoneyLog> {
@Autowired
private MoneyLogService moneyLogService;
@Autowired
private RedisTemplate redisTemplate;
/**
* 数据增加变更
* @param moneyLog
*/
@Override
public void insert(MoneyLog moneyLog) {
//查询用户的抢红包列表
List<MoneyLog> moneyLogs = moneyLogService.list(moneyLog.getUsername());
//将数据存入到Redis
redisTemplate.boundHashOps("UserMoneyLog").put(moneyLog.getUsername(), moneyLogs);
//更新nginx一级缓存同样道理
}
}
配置Canal地址:
#Canal配置
canal:
server: 192.168.211.141:11111
destination: example
七 RESTful站点安全终极解决方案
lua脚本解决基于RESTFul开发安全风控解决方案:【缓存穿透】、【缓存击穿】、【缓存雪崩】、【黑白名单】、 【定向日志收集】、【防止攻击】、【限流】、【熔断】
[站外图片上传中...(image-f07850-1623382960683)]
ngnix和lua脚本结合,用lua脚本对请求进行分析,然后降请求转发下去。
[站外图片上传中...(image-f6762e-1623382960683)]
1. lua执行http请求:
--输出json类型数据
ngx.header.content_type="application/json;charset=utf8"
local http = require "resty.http"
local httpc = http.new()
--执行请求
local res, err = httpc:request_uri("http://192.168.0.105:18082/api/userinfo/one", {
method = "GET",
body = "a=1&b=2",
headers = {
["Content-Type"] = "application/x-www-form-urlencoded",
},
--当前连接保存时间
keepalive_timeout = 60000,
--连接池数量
keepalive_pool = 10
})
if not res then ngx.say("failed to request: ", err) return end
ngx.say(res.body)
修改nginx.config
#api
location ~ /api {
content_by_lua_file /usr/local/openresty/nginx/lua/resthttp.lua;
}
八 Nginx缓存学习
Nginx处于Web网站服务最外层,而且支持浏览器缓存配置和后端缓存,用它做部分数据缓存效率更高。
8.1 实现浏览器缓存
8.2 Nginx清理缓存
如果不想采用缓存过期,采用第三方缓存清理模块,nginx_ngx_cache_purge 。在安装OpenRestry就已经实现了。
1. 配置清理缓存:
#清理缓存
location ~ /purge(/.*) {
#清理缓存
proxy_cache_purge openresty_cache $host$1$is_args$args;
}
2. 查看缓存:
每次请求 key 就可以删除指定缓存,我们可以先查看缓存文件的可以
[站外图片上传中...(image-c942ad-1623382960683)]
访问路径:http://ip+port/purge/user/wangwu
8.3 Lua脚本基本语法
百度吧
8.4 多级缓存架构
1. 用Java实现多级缓存:
[站外图片上传中...(image-86551f-1623382960683)]
1、用户请求经过Nginx
2、Nginx检查是否有缓存,如果Nginx有缓存,直接响应用户数据
3、Nginx如果没有缓存,则将请求路由给后端Java服务
4、Java服务查询Redis缓存,如果有数据,则将数据直接响应给Nginx,并将数据存入缓存,Nginx将数据响应给用户
5、如果Redis没有缓存,则使用Java程序查询MySQL,并将数据存入到Reids,再将数据存入到Nginx中
2. 用lua脚本实现多级缓存:
[站外图片上传中...(image-5d44f4-1623382960683)]
nginx+lua多级缓存架构搭建,用lua脚本实现连接redis和mysql。避免了tomcat并发能力瓶颈。
用户请求查询数据:
a 先查询nginx缓存,如果缓存存在直接响应,不存在直接用lua脚本查询redis。
b redis中有数据直接响应,并把缓存加载到nginx中。如果没有查询到缓存,查询MySQL。
c 查询到数据,响应用户,然后以次放入到redis和nginx缓存中。
Lua脚本连接MySQL:
--MySQL查询操作,封装成一个模块
--Java操作MySqL
--导入依赖包
local mysql = require "resty.mysql"
--配置数据源链接
local props = {
host = "192.168.211.141",
port = 3306,
database = "redpackage",
user = "root",
password = "123456"
}
--创建一个对象
local mysqldb = {}
--查询数据库
function mysqldb.query(sql)
--创建链接
local db = mysql:new()
--设置超时时间
db:set_timeout(10000)
db:connect(props)
--配置编码格式
db:query("SET NAMES utf8")
--查询数据库 "select * from activity_info where id=1"
local result = db:query(sql)
--关闭链接
db:close()
--返回结果集
return result
end
return mysqldb
Lua脚本连接Redis:
--操作Redis集群,封装成一个模块
--引入依赖库
local redis_cluster = require "resty.rediscluster"
--配置Redis集群链接信息
local config = {
name = "test",
serv_list = {
{ip="192.168.211.141", port = 7001},
{ip="192.168.211.141", port = 7002},
{ip="192.168.211.141", port = 7003},
{ip="192.168.211.141", port = 7004},
{ip="192.168.211.141", port = 7005},
{ip="192.168.211.141", port = 7006},
},
idle_timeout = 1000,
pool_size = 10000,
}
--定义一个对象
local lredis = {}
--创建set()添加数据方法
function lredis.set(key,value)
--1)打开链接
local red = redis_cluster:new(config)
red:init_pipeline()
--2)执行命令【set】
red:set(key,value)
red:commit_pipeline()
--3)关闭链接
red:close()
end
--创建查询数据get()
function lredis.get(key)
--1)打开链接
local red = redis_cluster:new(config)
red:init_pipeline()
--2)执行命令【set】
red:get(key)
local result = red:commit_pipeline()
--3)关闭链接
red:close()
--4)返回结果集
return result
end
return lredis
Lua脚本执行业务:
--多级缓存流程操作
--1)Lua脚本查询Nginx缓存
--2)Nginx如果没有缓存
--2.1)Lua脚本查询Redis
--2.1.1)Redis如果有数据,则将数据存入到Nginx缓存,并响应用户
--2.1.2)Redis没有数据,Lua脚本查询MySQL
-- MySQL有数据,则将数据存入到Redis、Nginx缓存[需要额外定义],响应用户
--3)Nginx如果有缓存,则直接将缓存响应给用户
--响应数据为JSON类型
ngx.header.content_type="application/json;charset=utf8"
--引入依赖库
--cjson:对象转JSON或者JSON转对象
local cjson = require("cjson")
local mysql = require("mysql")
local lrredis = require("redis")
--获取请求参数ID http://192.168.211.141/act?id=1
local id = ngx.req.get_uri_args()["id"];
--加载本地缓存
local cache_ngx = ngx.shared.act_cache;
--组装本地缓存的key,并获取nginx本地缓存
local ngx_key = 'ngx_act_cache_'..id
local actCache = cache_ngx:get(ngx_key)
--如果nginx中没有缓存,则查询Redis集群缓存
if actCache == "" or actCache == nil then
--从Redis集群中加载数据
local redis_key = 'redis_act_'..id
local result = lrredis.get(redis_key)
--Redis中数据为空,查询数据库
if result[1]==nil or result[1]==ngx.null then
--组装SQL语句
local sql = "select * from activity_info where id ="..id
--执行查询
result = mysql.query(sql)
--数据不为空,则添加到Redis中
if result[1]==nil or result[1]==ngx.null then
ngx.say("no data")
else
--数据添加到Nginx缓存和Redis缓存
lrredis.set(redis_key,cjson.encode(result))
cache_ngx:set(ngx_key, cjson.encode(result), 2*60);
ngx.say(cjson.encode(result))
end
else
--将数据添加到Nginx缓存中
cache_ngx:set(ngx_key, result, 2*60);
--直接输出
ngx.say(result)
end
else
--输出缓存数据
ngx.say(actCache)
end
nginx配置:
#活动查询
location /act {
content_by_lua_file /usr/local/openresty/nginx/lua/activity.lua;
}
8.5 红包雨案例
8.5.1 红包场景概述
1. 抢红包的特点:
a 并发量大(抢红包,白捡的都去抢。所以人很多)
b 按照时间段来发放(生活中的红包就是在几个小时发几波)
c 抢的红包肯定是不能超过预设的总金额
e 抢红包肯定是先到先得。(抢红包的公平性)
f 在发红包时候,可以追加红包的数量和延迟抢红包时间。
2. 抢红包策略:
[站外图片上传中...(image-c0d688-1623382960683)]
老板规定发金额和发的个数确定好,通过算法得出每个红包金额,然后分批次将红包放入到redis中。每个人抢红包直接从redis中拿就可以了。因为redis是单线程所有每次只能一个用户取到,所以避免了一个红包多个人抢。传说中解决超卖。
8.5.2 红包放入缓存队列中
1. 定时将红包导入缓存队列:
初始化读取:
创建容器监听类 ,让该类实现接口 ApplicationListener ,当容器初始化完成后会调用onApplicationEvent 方法。然后去去到数据到redis队列中。
@Component
public class MoneyPushTask implements ApplicationListener<ContextRefreshedEvent>{
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
//清空历史数据
//加载新的数据
}
}
定时加载:
可以使用定时任务,定时的更新红包数量
8.5.3 解决大量的人抢红包导致服务器崩溃
当有大量人去抢红包,服务器很有可能会崩溃。采用队列削峰。
[站外图片上传中...(image-fac8bc-1623382960683)]
大量用户来抢红包时候,使用Lua脚本将将请求放到缓存队列中,服务端处理队列中的请求。在lua脚本中可以导入 lua-resty-jwt模块,用来安全验证。
8.6 Nginx限流
我们采用多级缓存的模式,但是当用户反复刷新页面没有必要让所有请求到达服务器。还有一些恶意的攻击请求,也要避免请求到达服务器。限流是保护系统的一种方式。
1. 控制速率(控制请求数量和请求速度):
[站外图片上传中...(image-3f59f7-1623382960683)]
水过来先放到桶里,然后匀速的将水流出。当流入桶里水过大,水就直接溢出了。用户请求类似这样原理,当请求来了先放到缓冲中然后匀速的到达服务器,
当请求过多,直接拒绝请求。
nginx配置文件:
# 配置限制流缓存空间
limit_req_zone $binary_remote_addr zone=contentRateLimit:10m rate=2r/s;
# 配置限流
limit_req zone=contentRateLimit;
# 上面参数的解释
binary_remote_addr 是一种key,表示基于 remote_addr(客户端IP) 来做限流,binary_ 的目的是压缩内存占用 量。 zone:定义共享内存区来存储
访问信息, contentRateLimit:10m 表示一个大小为10M,名字为contentRateLimit的 内存区域。1M能存储16000 IP地址的访问信息,10M可以存储
16W IP地址访问信息。 rate 用于设置大访问速率,rate=10r/s 表示每秒多处理10个请求。Nginx 实际上以毫秒为粒度来跟踪请求信息,因 此 10r/s
实际上是限制:每100毫秒处理一个请求。这意味着,自上一个请求处理完后,若后续100毫秒内又有请求到达,将 拒绝处理该请求.我们这里设置
成2 方便测试。
注意:当设置了限流但是并发上来了,这样大部分请求都会被拒绝。
lilimit_req zone=contentRateLimit burst=4 nodelay;
2. 控制并发连接数(限制某个ip连接服务器的个数,连接服务器的总数):
利用limit_conn_zone和limit_conn两个指令,限制某一个ip连接数。
nginx参数配置:
#根据IP地址来限制,存储内存大小10M
limit_conn_zone $binary_remote_addr zone=addr:1m;
limit_conn addr 2;
# 参数解释
limit_conn_zone $binary_remote_addr zone=addr:10m; 表示限制根据用户的IP地址来显示,设置存储地址为的 内存大小10M
limit_conn addr 2; 表示 同一个地址只允许连接2次。
限制某个ip连接数量。限制连接的总个数
#IP限流
limit_conn_zone $binary_remote_addr zone=perip:10m;
#根据server的名字限流
limit_conn_zone $server_name zone=perserver:10m;
#单个客户端ip与服务器的连接数.
limit_conn perip 10;
#限制与服务器的总连接数
limit_conn perserver 100;