redis支持更丰富的数据类型
不仅支持简单的K/V类型,同时还提供list,set,zset,hash等数据类型,而memcache只支持string类型
redis支持数据的持久化,可以将内存的数据保存在磁盘中,重启的时候再次加载使用,而Memecache把数据全部存在内存中
集群模式
memcached没有原生的集群模式,需要依靠客户端来实现向集群中分片写入数据,redis原生支持cluster模式
memcached是多线程,非阻塞io复用网络模型,redis是单线程的多路IO复用模型
Memcache的key不能超过250个字节,value不能超过1M字节
Memcache的key的最大失效时间为30天
不支持主从同步
I/O 多路复用其实是在单个线程中通过记录跟踪每一个sock(I/O流) 的状态来管理多个I/O流。
IO多路复用模型是建立在内核提供的多路分离函数select基础之上的,使用select函数可以避免同步非阻塞IO模型中轮询等待的问题。
如上图所示,用户线程发起请求的时候,首先会将socket添加到select中,这时阻塞等待select函数返回。当数据到达时,select被激活,select函数返回,此时用户线程才正式发起read请求,读取数据并继续执行。
使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的I/O请求。用户可以注册多个socket,然后不断地调用select读取被激活的socket,即可达到在同一个线程内同时处理多个I/O请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。
该方法的能够同时监控多个文件描述符的可读可写情况,当其中的某些文件描述符可读或者可写时,select
方法就会返回可读以及可写的文件描述符个数。
I/O多路复用模型使用了Reactor设计模式实现了这一机制。通过Reactor的方式,可以将用户线程轮询I/O操作状态的工作统一交给handle_events事件循环进行处理。用户线程注册事件处理器之后可以继续执行做其他的工作(异步),而Reactor线程负责调用内核的select函数检查socket状态。当有socket被激活时,则通知相应的用户线程(或执行用户线程的回调函数),执行handle_event进行数据读取、处理的工作。由于select函数是阻塞的,因此多路I/O复用模型也被称为异步阻塞I/O模型。注意,这里的所说的阻塞是指select函数执行时线程被阻塞,而不是指socket。一般在使用I/O多路复用模型时,socket都是设置为NONBLOCK的,不过这并不会产生影响,因为用户发起I/O请求时,数据已经到达了,用户线程一定不会被阻塞。
I/O多路复用这个概念被提出来以后, select是第一个实现 (1983 左右在BSD里面实现的)。
一、select 被实现以后,很快就暴露出了很多问题。
“If a file descriptor being monitored by select() is closed in another thread, the result is unspecified”
霸不霸气
二、于是14年以后(1997年)一帮人又实现了poll, poll 修复了select的很多问题,比如
其实拖14年那么久也不是效率问题, 而是那个时代的硬件实在太弱,一台服务器处理1千多个链接简直就是神一样的存在了,select很长段时间已经满足需求。
但是poll仍然不是线程安全的, 这就意味着,不管服务器有多强悍,你也只能在一个线程里面处理一组I/O流。你当然可以那多进程来配合了,不过然后你就有了多进程的各种问题。
于是5年以后, 在2002, 大神 Davide Libenzi 实现了epoll.
三、epoll 可以说是I/O 多路复用最新的一个实现,epoll 修复了poll 和select绝大部分问题, 比如:
可是epoll 有个致命的缺点,只有linux支持。比如BSD上面对应的实现是kqueue。
其实有些国内知名厂商把epoll从安卓里面裁掉这种脑残的事情我会主动告诉你嘛。什么,你说没人用安卓做服务器,尼玛你是看不起p2p软件了啦。
而ngnix 的设计原则里面, 它会使用目标平台上面最高效的I/O多路复用模型咯,所以才会有这个设置。一般情况下,如果可能的话,尽量都用epoll/kqueue吧。
redis的io模型主要是基于epoll实现的,不过它也提供了 select和kqueue的实现,默认采用epoll。
那么epoll到底是个什么东西呢? 其实只是众多i/o多路复用技术当中的一种而已,但是相比其他io多路复用技术(select, poll等等),epoll有诸多优点:
最基本的数据类型,二进制安全
set name "redis" # 设置一个key为name,值为redis的字符串类型的key-value
get name # 获取name这个key对应的值
set count 1 # 设置一个key为count值为1的数据
get count # 返回字符串 '1'
incr count #对count执行 + 1操作,并且是原子性的
保存字符串的结构
/*
* 保存字符串对象的结构
*/
struct sdshdr {
// buf 中已经占用空间的长度
int len;
// buf 中剩余可用空间的长度
int free;
// 数据空间
char buf[];
}
试用场景
1. 缓存功能:String字符串是最常用的数据类型,不仅仅是Redis,各个语言都是最基本类型,因此,利用Redis作为缓存,配合其他数据库作为存储层,利用Redis支持高并发的特点,可以大大加快系统的读写速度,以及降低后端数据库的压力
2. 计数器: 许多系统都会使用Redis作为系统的实时计数器,可以快速实现计数和查询的功能。而且最终的数据结果可以按照特定的时间落底到数据库或者其他存储介质当中进行永久保存。
3.共享用户Session: 用户重新刷新一次界面,可能需要访问一下数据库进行重新登录,或者访问页面缓存Cookie,但是可以利用Redis将用户的Session集中管理,在这种模式下只需要保证Redis的高可用,每次用户Session的更新和获取都可以快速完成,大大提高效率。
操作
hmset Lilei name 'Lilei' age 26 title 'Senior' # 创建一个Lilie的hash字典
hget Lilei age #获取lilei的age
hget Lilei title #获取lilei的title
hset Lilei title 'Pricipal' # 更新lilei的title
按照String元素插入顺序排序
操作
lpush myList aaa
lpush myList bbb # 返回当前列表元素的数量
lrange myList 0 10 # 从左边开始取,取十个,后进先出
String元素组成的无序集合,通过哈希表实现,不允许重复
操作
sadd myset 111 # 成功返回1,不成功,即已经存在,返回0
smembers myset # 查看myset内的元素,无序
通过分数(double类型)来为集合中的成员进行从小到大的排序
操作
zadd myzset 3 adc
zadd myzset 1 add
zadd myzset 2 ada # 第一个为分数,第二个为值,1表示成功 0表示失败
zrangebyscore myzset 0 10 # 按照分数从小到大取元素,取10个
用来计数
HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定 的、并且是很小的。
但是,因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。
PFADD runoobkey "redis" # 添加
PFCOUNT runoobkey # 查看个数
用于支持存储地理位置信息的
keys
查找所有符合指定模式pattern的key
dbsize # 查找当前库中的key的数量
keys k1* # 返回所有以k1开头的key的值,但是会阻塞
缺点:
1、一次性返回所有符合条件的key
2、键的数量过大会使服务卡顿
SCAN(推荐)
SCAN cursor 【MATCH pattern】 【COUNT count】 # cursor 游标,pattern 模式,count 数量
优点:
1、基于游标的迭代器,需要基于 上一次的游标延续之前的迭代过程
2、以0作为有标开始一次新的迭代,直到命令返回游标0完成一次遍历。
3、不保证每次执行都返回某个给定数量的元素,支持模糊查询
4、一次返回的数量不可控,只能是大概率符合count参数
scan 0 match k1* count 10 #开始迭代,返回以k1开头的key,希望数量为10个
# 返回结果为两个 第一个是下一个的游标,第二个是一个list 结果集
互斥性
任何时间,只能有一个客户端获取到锁
安全性
锁只能被持有该锁的客户端删除,
死锁
获取锁的客户端不正常宕机,其他客户端无法获取到锁
容错
当部分节点宕机的时候,客户端仍然能获取锁,释放锁
setnx key value
如果key不存在,创建并赋值
时间复杂度为O(1)
设置成功返回1,设置失败返回0
解决setnx长期有效问题:expire key timeout
设置key的过期时间
setnx lockKey 1
使用set 操作实现setnx + expire的一步原子性操作
set key value 【EX seconds】【PX milliseconds】【NX|XX】 # 设置键的过期时间为seconds秒 设置键的过期时间为milliseconds毫秒,nx只在键不存在的时,才对键进行设置操作(等同于setnx), xx只在键已经存在时,才对键进行设置操作。
# 成功返回ok,失败返回nil
set lockTarget 12345 ex 10 nx #设置key为lockTarget,值为12345的数据 设置过期时间为10s.如果不存在才设置。
## 大量key过期时间一致的问题
问题: 如果大量的key同一时间过期,会导致卡顿现象,
解决方案:在设置key的过期时间的时候,给每个key加上随机值。这样就能分散控制key的过期时间。
## 使用redis实现异步队列
1. 使用List作为队列,Rpush生产消息,LPOP消费消息
~~~shell
rpush testList aaa # 生产消息
lpop testList # 消费消息
blpop testList timeout #阻塞直到队列有消息或者超时
缺点:
没有等待有值就去消费,
可以通过在应用层引入sleep机制去调用lpop去重试
只能实现一对一的模式,消费过一次后就没有了
pub/sub 主题订阅者模式
实现一对多的模式
订阅者可以定义任意数量的主题(topic)
subscribe myTopic # 订阅该主题的数据,不要求该主题存在
publish myTopic 'hello' # 发布消息,在mytopic中
缺点:
雪花算法(SnowFlake),是Twitter开源的分布式id生成算法,其核心思想是:使用一个64bit的long型的数字作为全局唯一的id,在分布式系统中的应用十分广泛,且ID引入了时间戳,基本上保持了自增。
在这64个bit中,其中1个bit是不用的,然后其中的41bit作为毫秒数,用10bit作为工作机器id,12bit作为序列号。
这个图片分析如下
第一部分的1bit为什么不用
因为二进制里第一个bit如果为1,那么就是负数,但是我们生成的id都是正数,所以第一个bit统一为0
第二部分41bit,表示时间错
41 bit 可以表示的数字多达 2^41 - 1,也就是可以标识 2 ^ 41 - 1 个毫秒值,换算成年就是表示 69 年的时间
第三部分和第四部分
记录工作机器id,代表的是这个服务最多可以部署在2^10台机器上,也就是1024台服务器
但是 10 bit 里 5 个 bit 代表机房 id,5 个 bit 代表机器 id。意思就是最多代表 2 ^ 5 个机房(32 个机房),每个机房里可以代表 2 ^ 5 个机器(32 台机器),也可以根据自己公司的实际情况确定。
最后一部分
这个是用来记录同一个毫秒内产生的不同 id。
12 bit 可以代表的最大正整数是 2 ^ 12 - 1 = 4096,也就是说可以用这个 12 bit 代表的数字来区分同一个毫秒内的 4096 个不同的 id。
(1)高性能高可用:生成时不依赖于数据库,完全在内存中生成。
(2)容量大:每秒中能生成数百万的自增ID。
(3)ID自增:存入数据库中,索引效率高。
package com.fcbox.fms.util;
public class IdWorker {
//因为二进制里第一个 bit 为如果是 1,那么都是负数,但是我们生成的 id 都是正数,所以第一个 bit 统一都是 0。
//机器ID 2进制5位 32位减掉1位 31个
private long workerId;
//机房ID 2进制5位 32位减掉1位 31个
private long datacenterId;
//代表一毫秒内生成的多个id的最新序号 12位 4096 -1 = 4095 个
private long sequence;
//设置一个时间初始值 2^41 - 1 差不多可以用69年
private long twepoch = 1585644268888L;
//5位的机器id
private long workerIdBits = 5L;
//5位的机房id
private long datacenterIdBits = 5L;
//每毫秒内产生的id数 2 的 12次方
private long sequenceBits = 12L;
// 这个是二进制运算,就是5 bit最多只能有31个数字,也就是说机器id最多只能是32以内
private long maxWorkerId = -1L ^ (-1L << workerIdBits);
// 这个是一个意思,就是5 bit最多只能有31个数字,机房id最多只能是32以内
private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
private long workerIdShift = sequenceBits;
private long datacenterIdShift = sequenceBits + workerIdBits;
private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
private long sequenceMask = -1L ^ (-1L << sequenceBits);
//记录产生时间毫秒数,判断是否是同1毫秒
private long lastTimestamp = -1L;
public long getWorkerId(){
return workerId;
}
public long getDatacenterId() {
return datacenterId;
}
public long getTimestamp() {
return System.currentTimeMillis();
}
public IdWorker(long workerId, long datacenterId, long sequence) {
// 检查机房id和机器id是否超过31 不能小于0
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(
String.format("worker Id can't be greater than %d or less than 0",maxWorkerId));
}
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException(
String.format("datacenter Id can't be greater than %d or less than 0",maxDatacenterId));
}
this.workerId = workerId;
this.datacenterId = datacenterId;
this.sequence = sequence;
}
// 这个是核心方法,通过调用nextId()方法,让当前这台机器上的snowflake算法程序生成一个全局唯一的id
public synchronized long nextId() {
// 这儿就是获取当前时间戳,单位是毫秒
long timestamp = timeGen();
if (timestamp < lastTimestamp) {
System.err.printf(
"clock is moving backwards. Rejecting requests until %d.", lastTimestamp);
throw new RuntimeException(
String.format("Clock moved backwards. Refusing to generate id for %d milliseconds",
lastTimestamp - timestamp));
}
// 下面是说假设在同一个毫秒内,又发送了一个请求生成一个id
// 这个时候就得把seqence序号给递增1,最多就是4096
if (lastTimestamp == timestamp) {
// 这个意思是说一个毫秒内最多只能有4096个数字,无论你传递多少进来,
//这个位运算保证始终就是在4096这个范围内,避免你自己传递个sequence超过了4096这个范围
sequence = (sequence + 1) & sequenceMask;
//当某一毫秒的时间,产生的id数 超过4095,系统会进入等待,直到下一毫秒,系统继续产生ID
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0;
}
// 这儿记录一下最近一次生成id的时间戳,单位是毫秒
lastTimestamp = timestamp;
// 这儿就是最核心的二进制位运算操作,生成一个64bit的id
// 先将当前时间戳左移,放到41 bit那儿;将机房id左移放到5 bit那儿;将机器id左移放到5 bit那儿;将序号放最后12 bit
// 最后拼接起来成一个64 bit的二进制数字,转换成10进制就是个long型
return ((timestamp - twepoch) << timestampLeftShift) |
(datacenterId << datacenterIdShift) |
(workerId << workerIdShift) | sequence;
}
/**
* 当某一毫秒的时间,产生的id数 超过4095,系统会进入等待,直到下一毫秒,系统继续产生ID
* @param lastTimestamp
* @return
*/
private long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
//获取当前时间戳
private long timeGen(){
return System.currentTimeMillis();
}
/**
* main 测试类
* @param args
*/
public static void main(String[] args) {
System.out.println(1&4596);
System.out.println(2&4596);
System.out.println(6&4596);
System.out.println(6&4596);
System.out.println(6&4596);
System.out.println(6&4596);
// IdWorker worker = new IdWorker(1,1,1);
// for (int i = 0; i < 22; i++) {
// System.out.println(worker.nextId());
// }
}
}
保存某个时间点的全量数据快照
@ 相关配置
save 900 1 # 900s内有一条是写入指令,就进行一次快照
save 300 10 # 300s内有十条写入,就进行一次快照
save 60 10000 # 60s内有一万条写入,就进行一次快照
stop-writes-on-bgsave-error yes # 设置成yes表示当备份进程出错时,主进程就停止接收新的写入操作。
rdbcompression yes # 在备份时将rdb文件压缩后在进行保存,建议设置成no,因为redis属于cpu密集型服务器,在开启压缩后会带来cpu压力
@指令
SAVE # 阻塞redis的服务器进程,知道RDB文件被创建完成,很少使用。
BGSAVE # Fork出一个子进程来创建RDB文件,不阻塞服务器进程。
lastsave # 返回上次保存的时间戳
@ 自动触发RDB持久化的方式
系统调用fork()创建进程,并且实现了Copy-on-Write
@Copy-on-Write(写时复制)
如果有多个调用者同时要求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本给该调用者,而其他调用者所见到的最初的资源仍然保持不变。
@ 缺点
@ 记录除了查询以外的所有变更数据库状态的指令
@ 以append的形式保存到AOF文件中(增量)
@ 默认是关闭的
appdendonly no # 默认是关闭的 在redis.conf中更改为yes可打开AOF方式
appendfsync everysec # 配置更新的方式,--always(一旦发生变化就将缓存变化写入AOF中) -- everysec(推荐)(将缓存区的内容每一秒都写入AOF文件中) -- no(将写入的操作交给操作系统,一般系统会在缓冲区被填满才同步到磁盘中)
@ 日志重写解决AOF文件大小不断增大的问题
原理:
@ Pipeline类似于Linux的管道
@ Redis基于请求/相应模式,单个请求处理需要一一应答
@ Pipeline批量执行指令,节省多次IO往返的时间
@ 有顺序依赖的指令建议分批发送
使用redis-benchmark进行压测的时候可以发现影响redis的QPS峰值的一个重要因素是pipeline批次指令的数目。
选举过程
主观下线
集群中的每一个Sentinel节点会对redis集群的所有节点发心跳包检测节点是否正常。如果一个节点在down-after-milliseconds
时间内没有回复Sentinel节点的心跳包,则该节点被Sentinel节点主观下线
客观下线
当节点被一个sentinel节点标记为主观下线后,并不意味着这个节点真正的下线了,还需要Sentinel集群的其他sentinel节点共同判断为主观下线才行
该sentinel节点会询问其他sentinel节点,如果sentinel集群中超过quorum
数量的sentinel节点也认为该redis节点主观下线了,则该redis节点客观下线
如果客观下线的redis节点是从节点或者Sentinel节点,则操作结束,如果客观下线的redis节点为主节点,则开始故障转移,从从节点中选举一个节点升级为主节点
Sentinel集群选举Leader
如果需要从redis集群中选举一个节点为主节点,首先需要从sentinel集群中选举一个Sentinel节点作为Leader
每一个Sentinel节点都可以成为Leader,当一个Sentinel节点确认redis集群的主节点主观下线后,会请求其他Sentinel节点要求将自己选举为Leader。被请求的Sentinel节点如果没有同意过其他Sentinel节点的选举请求,则同意该请求(选举票数+1),否则不同意
如果一个sentinel节点获得的选举票数达到Leader的最低票数Math.max(quorum,Sentinel节点数 / 2 + 1)
, 则该sentinel节点选举为Leader,否则重新进行选举
当Sentinel集群选举出Sentinel Leader后,由Sentinel Leader从redis从节点中选择一个redis节点作为主节点:
slave-priority
最大的节点最为主节点,如不存在则继续runId
(redis每次启动的时候生成随机的runId作为redis的标识)最小的从节点作为主节点为什么Sentinel集群至少3个节点
一个Sentinel节点选举为Leader的最低票数为quorum
和Sentinel节点数/2+1
的最大值,如果Sentinel集群只有两个Sentinel节点,则
Sentinel节点数/2 + 1
= 2/2 + 1
= 2
即Leader最低票数至少为2,当该Sentinel集群中由一个Sentinel节点故障后,仅剩的一个Sentinel节点是永远无法成为Leader。
也可以由此公式可以推导出,Sentinel集群允许1个Sentinel节点故障则需要3个节点的集群;允许2个节点故障则需要5个节点集群。
@ 如何从海量数据中快速找到所需
经典的缓存 + 数据库读写的模式
定期删除
定期随机抽取一些设置了过期时间的key,检查是否过期
惰性删除
不主动删,等查询的时候查看是否过期
内存淘汰机制
Lua 脚本功能是 Reids在 2.6 版本的最大亮点, 通过内嵌对 Lua 环境的支持, Redis 解决了长久以来不能高效地处理 CAS (check-and-set)命令的缺点, 并且可以通过组合使用多个命令, 轻松实现以前很难实现或者不能高效实现的模式。
Lua脚本类似于Redis事务,有一定的原子性,不会被其他命令插队。可以完成一些Redis事务性的操作.
编写脚本去支持获取库存和更改库存的操作
提前给redis中嵌入部分数据,在提供服务
肯定不可能将所有数据都写入redis,因为数据量太大了,第一耗费的时间太长了,第二redis根本就容纳不下所有的数据
需要更具当天的具体访问情况,试试统计出频率较高的热数据
然后将访问频率较高的热数据写入到redis,肯定是热数据也比较多,我们也得多个服务并行的读取数据去写,并行的分布式的缓存预热
然后将嵌入的热数据的redis对外提供服务,这样就不至于冷启动,直接让数据库奔溃了
具体的实时方案:
要统计出来当前最新的实时的热数据是哪些,我们就得将商品详情页访问的请求对应的流量,日志,实时上报到kafka中,
优先用内存中的一个LRUMap去存放,性能高,而且没有外部依赖
否则的话,依赖redis,我们就是要防止reids挂掉数据丢失的情况,就不合适了;用mysql,扛不住高并发读写;用hbase,hadoop生态系统,维护麻烦,太重了,其实我们只要统计出一段时间访问最频繁的商品,然后对它们进行访问计数,同时维护出一个前N个访问最多的商品list即可
计算好每个task大致要存放的商品访问次数的数量,计算出大小,然后构建一个LURMap,apache commons collections有开源的实现,设定好map的最大大小,就会自动根据LRU算法去剔除多余的数据,保证内存使用限制,即使有部分数据被干掉了,然后下次来重新开始技术,也没什么关系,因为如果他被LRU算法干掉,那么它就不是热数据,说明最近一段时间很少访问,
所谓“动静分离”,其实就是把用户请求的数据(如 HTML 页面)划分为“动态数据”和“静态数据”。
简单来说,“动态数据”和“静态数据”的主要区别就是看页面中输出的数据是否和 URL、浏览者、时间、地域相关,以及是否含有 Cookie 等私密数据。比如说:
很多媒体类的网站,某一篇文章的内容不管是你访问还是我访问,它都是一样的。所以它就是一个典型的静态数据,但是它是个动态页面。
我们如果现在访问淘宝的首页,每个人看到的页面可能都是不一样的,淘宝首页中包含了很多根据访问者特征推荐的信息,而这些个性化的数据就可以理解为动态数据了。
分离了动静数据,我们就可以对分离出来的静态数据做缓存,有了缓存之后,静态数据的“访问效率”自然就提高了。
如何对静态数据进行缓存
第一,你应该把静态数据缓存到离用户最近的地方。静态数据就是那些相对不会变化的数据,因此我们可以把它们缓存起来。缓存到哪里呢?常见的就三种,用户浏览器里、CDN 上或者在服务端的 Cache 中。你应该根据情况,把它们尽量缓存到离用户最近的地方。
第二,静态化改造就是要直接缓存 HTTP 连接。相较于普通的数据缓存而言,你肯定还听过系统的静态化改造。静态化改造是直接缓存 HTTP 连接而不是仅仅缓存数据,如下图所示,Web 代理服务器根据请求 URL,直接取出对应的 HTTP 响应头和响应体然后直接返回,这个响应过程简单得连 HTTP 协议都不用重新组装,甚至连 HTTP 请求头也不需要解析。
第三,让谁来缓存静态数据也很重要。不同语言写的 Cache 软件处理缓存数据的效率也各不相同。以 Java 为例,因为 Java 系统本身也有其弱点(比如不擅长处理大量连接请求,每个连接消耗的内存较多,Servlet 容器解析 HTTP 协议较慢),所以你可以不在 Java 层做缓存,而是直接在 Web 服务器层上做,这样你就可以屏蔽 Java 语言层面的一些弱点;而相比起来,Web 服务器(如 Nginx、Apache、Varnish)也更擅长处理大并发的静态文件请求。
如何做动静分离的改造
静态数据的处理
动态数据的处理
ESI 方案(或者 SSI)
即在 Web 代理服务器上做动态内容请求,并将请求插入到静态页面中,当用户拿到页面时已经是一个完整的页面了。这种方式对服务端性能有些影响,但是用户体验较好。
CSI 方案
即单独发起一个异步 JavaScript 请求,以向服务端获取动态内容。这种方式服务端性能更佳,但是用户端页面可能会延时,体验稍差。
本质上是位数组,存入布隆过滤器的数据只占一位。缺点是其返回的结果是概率性的,而且不是非常准确。
当元素加入到布隆过滤器的时候。
使用布隆过滤器中的哈希函数对元素值进行计算,得到哈希值(有几个哈希函数得到几个哈希值)
根据哈希值,在位数组中吧对应下标的值置为1
当我们需要判断一个元素是否存在于布隆过滤器的时候,进行如下操作
原因
大量的key在同一时间失效,并且此时大量的请求都进来,直接请求到数据库中,导致数据库崩溃
方法
令牌桶算法
原理:系统以一个恒定的速度往桶中放入令牌,如果请求需要被处理,则需要先从桶里获取一个令牌,当桶中没有令牌可取的时候,则拒绝服务,当桶满的时候,新添加的令牌被丢弃掉或者拒绝
public class RateLimiterDemo{
// 每一秒产生5个令牌,每200毫秒产生一个
private static RateLimiter limiter = RateLimiter.create(5);
public static void exec(){
// 消费一个令牌,当桶中有足够的令牌时,则直接返回0,否则阻塞,直到有可用的令牌数才返回,返回的值为阻塞的时间。
limiter.acquire(1);
try{
// 处理核心逻辑
TimeUnit.SECONDS.sleep(1);
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
漏桶算法
主要目的是控制数据注入到网络的速率,平滑网络上的突发流量,数据可以以任意速度流入到漏桶中。漏桶算法提供了一种机制,通过它,突发流量可以被整形以便为网络提供一个稳定的流量。 漏桶可以看作是一个带有常量服务时间的单服务器队列,如果漏桶为空,则不需要流出水滴,如果漏桶(包缓存)溢出,那么水滴会被溢出丢弃
计数器限流法
public class CountRateLimiterDemo2 {
private static Semaphore semphore = new Semaphore(5);
public static void exec() {
if(semphore.getQueueLength()>100){
System.out.println("当前等待排队的任务数大于100,请稍候再试...");
}
try {
semphore.acquire();
// 处理核心逻辑
TimeUnit.SECONDS.sleep(1);
System.out.println("--" + System.currentTimeMillis() / 1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semphore.release();
}
}
}
使用Semaphore信号量来控制并发执行的次数,如果超过域值信号量,则进入阻塞队列中排队等待获取信号量进行执行。如果阻塞队列中排队的请求过多超出系统处理能力,则可以在拒绝请求。
如果是瞬时的高并发,可以使请求在阻塞队列中排队,而不是马上拒绝请求,从而达到一个流量削峰的目的。
请求缓存和数据库中都没有的数据,如id 为 -1的值,redis中不存在,相当于绕过redis,每次都是请求数据库,导致数据库压力过大,击垮数据库,基本上都是攻击者
方法
接口层进行校验
BoomFilter(布隆过滤器)
使用高效的数据结构和算法判断出这个key是否在数据库中,不存在直接return,存在就查DB,刷新缓存再return
布隆过滤器的实现方式
将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力
另外一种方式,如果一个查询的结果为空,我们仍然把这个空结果进行缓存,但他的过期时间很短
原因
类似于雪崩,雪崩是大面积的缓存失效,打崩DB,缓存击穿是一个key很热点,在不停的扛着大并发,当这个key失效的瞬间,持续的大并发击穿缓存,请求数据库
方法
事前
redis高可用,主从 + 哨兵, redis cluster,避免全盘崩溃
事中
本地echache缓存,hystrix限流 + 降级,避免mysql被打死
事后
redis持久化RDB + AOF,一旦重启,自动从磁盘上加载数据,回复缓存数据。