高级篇章
概念和理论
Redis的单线程
在Redis版本4之后R已经变成多线程了,所谓的单线程指定是Redis命令工作线程,命令执行主线程执行,IO读写变成多线程,整体的Redis就是多线程,比如删除命令,执行删除,先删除主存中的值,开启另一个线程进行物理删除,还有其他线程进行RDB数据持久化。
因为Redis是基于内存操作,所以性能高,Redis的数据结构,导致查询很快,不需要多线程,不需要上下文切换;对应Redis系统,性能瓶颈是内存和网络带宽,不是CPU。
所以Redis为什么快
Redis是内存中数据结构存储系统,内存操作都是纳秒级别;Redis的工作线程是单线程执行,没有CPU上下切换;还有IO多路复用技术,可以让单线程处理高效的多个IO请求,跟Nginx一样,有一个单独的线程进行监听每个TCP连接,哪个有数据进行放行处理。就像一场考试,到点大家都交卷,有一个线程考官会跟大家说,谁答完举手,考官会根据举手的人执行放到队列中,到主线程执行任务就是交卷,依次的完成,再走出教室。
当我们Redis的CPU消耗不大,吞吐量不高的情况下,可以开启多线程配置:
# Redis一秒可以处理8万次,当我们查看吞吐量一秒才1万次,就是Redis性能没有充分利用
# 开启多线程配置,在Redis配置文件中
io-threads-do-reads yes # 默认是no,集群配置不建议开启,单机Redis可配置
io-threads 4 # 注意不要比当前机器核数高,4核的CPU,设置为2或3,如果为8核 CPU设置 6
BigKey
MoreKey案例
模拟生成一百万的redis数据
# 生成100W条redis写入到/tmp目录下的redisTest.txt文件中,在Linux命令行执行
for((i=1;i<=100*10000;i++)); do echo "set k$i v$i" >> /tmp/redisTest.txt ;done;
# 查看是否插入成功
more /tmp/redisTest.txt
# 通过redis管道的–pipe命令插入到Redis中
cat /tmp/redisTest.txt | redis-cli -h 127.0.0.1 -p 6379 -a 123456 --pipe
# 登录Redis,可以查看导入的数据
当key很庞大的时候,进行使用key * 命令
# 通过redis配置进行使用命令,如果哪个命令禁止使用,rename-command 命令语句 "",设置""可以
rename-command keys ""
rename-command flushdb ""
rename-command flushall ""
# 当我们禁止了keys *,我们可以使用scan 查询多个key
scan 0 match k* count 15
# 查询 从0开始(游标,如下一端的游标) match ‘要搜索的key,相当于模糊查询’ count 15 查15个
key很庞大的危害:
- 内存不均匀,集群迁移问题
- 超时删除,删除大key会有阻塞
- 网络IO阻塞
产生大key的场景:
- 社交类,当某个集合突然暴涨,例:粉丝暴涨
- 报表类,累积月的积累
防止redis的慢查询
# 设置string类型控制在10kb内,hash、list、set、zset个数不超过5000
# 通过redis-cli --bigkeys查询大key是哪个数据、占比(在Linux命令行执行)
redis-cli -h 127.0.0.1 -p 6379 -a redis密码 --bigkeys
# 每隔 100 条 scan 指令就会休眠 0.1s,ops 就不会剧烈抬升,但是扫描的时间会变长
redis-cli -h 127.0.0.1 -p 6379 -a redis密码 –-bigkeys -i 0.1
# 通过memory usage 查询某个key的字节数,(在Redis命令行执行)
# memory usage key名
删除大key
非字符串,不建议使用del删除,会阻塞删除;
在生产环境,我们使用非阻塞删除
在redis配置文件中更改,
lazyfree-lazy-eviction no
lazyfree-lazy-expire no
lazyfree-lazy-server-del yes
replica-lazy-flush yes
lazyfree-lazy-user-del yes
缓存双写一致性
一般我们查询,先查询Redis数据是否有数据,有的话直接返回,没有的话再查询Mysql数据库,再更新到缓存中。
这种对于多线程情况下,会造成多个线程请求到mysql数据库,会造成压力,这时我们采用双检加锁。
更新策略:
可以停机的话,服务降级、温馨提示维护、凌晨升级
不停机处理
先更新数据库,再更新缓存(多线程有问题)
# 问题 - 数据不一致 A线程更新数据库值为100 B线程更新数据库值为80 B线程更新缓存为80 A线程更新缓存为100
先更新缓存,再更新数据库
# 问题 mysql一般作为最总解释权,缓存更新成功,数据库更新失败,缓存的值不准确
先删除缓存,再更新数据库(多线程有问题)
# 问题 - 数据不一致 A线程删除缓存 B线程获取缓存失败,查询数据库,并重新更新缓存 B线程把旧值写回缓存中 A线程更新数据库,此时缓存中的数据有旧值的存在
解决方案 - 延迟双删
# 先删除缓存,再更新完数据库后,加上一定的睡眠时间(多个100ms即可),再来一次删除缓存; # 保证:更新完数据库的时间 + sleep的时间 大于 读取数据并写入换的时间 # 线程A 睡眠的时间,大于线程B读取数据再写入缓存的时间,就可以解决缓存双写一致性问题, # 但一般睡眠时间长,会影响吞吐量问题,我们可以起异步进行删除第二次的缓存
- 我们可以将要删除缓存和更新数据库的值,存放到消息队列中,
- 当我们成功删除缓存,并且更新数据库,我们可以将消息队列中此数据剔除
- 如果没有成功,从消息队列中再读该数据,再一次操作
- 如果失败,需要业务逻辑发送报错日志
canal
它可以监听mysql的变动且通知给Redis,它模拟Mysql主从的交互协议,将自己作为Mysql的从机 ,向Mysql主机发送dump协议,Mysql主机收到请求,开始推送 binary log 给 从机(即cancal)
官方地址:https://github.com/alibaba/canal
前提:
安装Mysql做为主机、配置Mysql
# 1 mysql的配置文件
[client]
default_character_set=utf8
[mysqld]
[mysqld]
# 主服务器唯一ID
server-id = 1
log-bin=自己本地的路径/data/mysqlbin # 启用二进制日志,日志的存放地址(前提把文件创建好)
log-err=自己本地的路径/data/mysqlerr # 启用二进制日志,错误日志存放地址(前提把文件创建好)
binlog_format=ROW # 选择 ROW 模式(默认也是ROW)
collation_server = utf8_general_ci
character_set_server = utf8
# 2 配置完,重启mysql
# 查看是否开启binlog的写入功能,值为on代表成功(在mysql环境下执行)
SHOW VARIABLES LIKE 'log_bin';
# 3.创建新用户,进行连接canal
use mysql;
select * from user; # 可查询有哪些用户
# 进行创建用户canal
DROP USER IF EXISTS 'canal'@'%';
create user 'canal用户'@'%' identified by 'canal密码';
GRANT REPLICATION SLAVE ON *.* TO 'canal用户'@'%';
ALTER USER 'canal用户'@'%' IDENTIFIED WITH mysql_native_password BY 'canal密码';
FLUSH PRIVILEGES;
# 4.配置完成
SELECT * FROM mysql.user; # 可查询有哪些用户
安装canal
下载地址:https://github.com/alibaba/canal/releases
在Assets有各种安装包,上传到对应的服务器上,找一个对应的文件目录,进行解压
# 1 解压
tar -zxvf canal.deployer-1.1.6.tar.gz
# 2 配置
# 修改 /xxx/conf/example 路径下的 instance.properties 文件
canal.instance.master.address=Mysql主键ip地址:端口号
canal.instance.dbUsername=canal用户
canal.instance.dbPassword=canal用户密码
:wq!
# 3 启动
# 切换到 /xxx/bin 目录下,执行 ./startup.sh 进行启动 (注意:前提需要有java8的环境配置)
# 4 看是否成功启动
# 切换到 /xxx/logs/canal 目录下 执行 cat canal.log
# 可以看到 the canal server is running now ... # 成功
# 切换到 /xxx/logs/example 目录下 执行 cat example.log
# 可以看到 start successful # 也代表启动成功
编写Canal客户端
在Canal项目中Redis有完整的代码。
hyperloglog
hyperloglog只统计一个集合不重复的元素的个数
pfadd hel 1 3 5 7 # 添加元素 ,然后统计个数是:4
pfadd he2 1 3 3 4 6 7 # 添加元素 ,然后统计个数是:5
pfcount hel # 打印he1个数 ,结果是 4
pfmerge he新 he1 he1 # 添加元素,多个元素
pfcount he新 # 打印he新个数 ,结果是 6
# 适合做统计个数,并不保留结果,添加元素,添加客户端iP
系统常见的统计
- 聚合统计(多个集合的交集、并集、差集)
- 排序统计(展示最新的列表、排行榜,如频繁更新的,使用ZSET)
- 二值统计(打开记录,只有0,1)
- 基数统计(统计集合不重复元素个数)
名词解释:
- UA:独立访客,IP地址,需要去重
- PA:页面浏览量
- DAU:日活,使用某个产品的用户数
- MAU:月活
项目中的使用
@Resource
private RedisTemplate redisTemplate;
// 插入key
Long k = redisTemplate.opsForHyperLogLog().add("key名", 值);
// 获取key,基数统计
Long 个数 = redisTemplate.opsForHyperLogLog().size("key名");
GEO
用二维的经纬度表示,经度范围 (-180, 180],纬度范围 (-90, 90],只要我们确定一个点的经纬度就可以名取得他在地球的位置
geoadd key1 经度1 纬度1 “天安门” 经度2 纬度2 “天安门2” # 添加经纬度坐标
ZRANGE key1 0 -1 # 获取全部的value值
# 如果返回值,有中文乱码,解决:redis -cli -a 123456 – raw (在外部,Linux命令行输入)
geopos key1 天安门 天安门2 # 返回经纬度
GEOHASH key1 天安门 天安门2 # 返回经纬度坐标的(base32编码)
GEODIST key1 天安门 故宫 km # 返回两个位置之间的距离,km是单位:千米 或 m:米
GEORADIUS 以给定的经纬度为中心, 返回键包含的位置元素当中,与中心的距离不超过给定最大距离的所有位置元素
# 例子:GEORADIUS key1 114.12 39.2 10 km withcoord withhash count 10 desc
# key (现在的经纬度) 10 (km或m) withcoord(会打印hash值)withhash(返回经纬度坐标)count 10(返回10条)
GEORADIUSBYMEMBER 它这个输入范围元素 例:GEORADIUS key1 天安门 10 km ...
项目中的使用
@Autowired
private RedisTemplate redisTemplate;
// 添加坐标
redisTemplate.opsForGeo().add(Key, map);
// 获取坐标
List<Point> position = redisTemplate.opsForGeo().position(Key, "map里的某个key");
// 获取两个位置之间的距离, 单位:KILOMETERS 千米
Distance dist = redisTemplate.opsForGeo().distance(Key, "map里的某个key1", "map里的某个key2",
RedisGeoCommands.DistanceUnit.KILOMETERS);
// 查找 根据某个坐标位置,显示附近有哪些,单位:KILOMETERS 千米
Circle circle = new Circle(坐标x,坐标y, Metrics.KILOMETERS.getMultiplier());
// 查询范围条件
RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeCoordinates().sortDescending().limit(10);
GeoResults<RedisGeoCommands.GeoLocation<String>> r = redisTemplate.opsForGeo().radius(Key, circle, args);
// 查找 根据某个坐标位置,显示附近有哪些
String member = "map里的某个key1";
RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance().sortAscending().limit(10);
// 半径 1 公里内
Distance distance = new Distance(1, Metrics.KILOMETERS);
GeoResults<RedisGeoCommands.GeoLocation<String>> r = redisTemplate.opsForGeo().radius(Key, member, distance, args);
bitmap
由0和1状态表现得二进制位的bit数组,用于判断用户是否登录过,如钉钉打卡
SETBIT key offset value # 将第offset的值设为value value只能是0或1 offset 从0开始
GETBIT key offset # 获得第offset位的值
STRLEN key # 得出占多少字节 超过8位后自己按照8位一组一byte再扩容
BITCOUNT key # 得出该key里面含有几个1
BITOP and destKey key1 key2 # 对一个或多个 key 求逻辑并,并将结果保存到 destkey
BITOP or destKey key1 key2 # 对一个或多个 key 求逻辑或,并将结果保存到 destkey
BITOP XOR destKey key1 key2 # 对一个或多个 key 求逻辑异或,并将结果保存到 destkey
BITOP NOT destKey key1 key2 # 对一个或多个 key 求逻辑非,并将结果保存到 destkey
布隆过滤器
它是一种专门用来解决去重问题的高级数据结构,就是一个大型bit数组和多个哈希函数组成,用来快速判断集合中是否存在元素。
常用
getbit key名 值 # 得出结果是 0或者1 来判断是否存在
优点:高效查询,占用内存空间少
缺点:不能删除元素,不够精准,如果删除对象,需要重构过滤器
布隆过滤器不能解决不能删除问题,有了第三方插件布谷鸟过滤器
缓存预热
缓存预热就是系统上线,将一些数据加载到Redis中,避免高并发的时候,用户访问到数据库中,将一些数据进行初始化到Redis中。
缓存雪崩
同一时间,请求量大,访问Redis失败,大量访问到Mysql造成服务压力。
机器故障,Redis挂掉了,解决该问题,部署Redis集群;或者Redis中大量key 同时过期。解决该问题:
- 将Key设置永不过期、或者不同的过期时间
- 多缓存结合预防雪崩,本地缓存(ehcache )+Redis缓存
- 服务降级,使用Hystrix 或者 sentinel 限流、降级
- 人民币 - 云数据库Redis,由阿里云托管、数据丢失
缓存穿透
缓存穿透就是请求去查询一条数据,先查redis,redis没有,再查mysql,mysql里也没有,如果这样的大量的这样情况,会造成服务器压力。
解决:就是如果mysql也没有,把缺省值更新到redis中,下次查询走redis,不再查mysql;但这也也有缺点,遇到恶性攻击,需要布隆过滤器,Google布隆过滤器Guava可以解决。
// 1.引包
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>23.0</version>
</dependency>
// 2.使用
BloomFilter<Integer> bloom = BloomFilter.create(Funnels.integerFunnel(), 1000, 0.03);
// 添加值
bloom.put(值);
// 获取值 存不存在
bloomFilter.mightContain(值);
缓存击穿
缓存击穿就是大量请求同时查询一个key时,然后这个key正好失效了,就会导致大量的请求去mysql查询,也是热点key突然都失效了,MySQL承受高并发量。
解决:对访问频繁的热点key,不设置过期时间;互斥更新,双检加锁;也可以使用差异失效时间解决,新建Redis,先更新缓存B再更新缓存A,查询列表,先查询缓存A,再查询缓存B,都没有再查询mysql。
分布式锁
Synchronized和Lock锁是本地单机锁,不适用分布式项目。使用单机锁会产生超卖问题。
我们一般使用Redis做分布式锁,加锁和解锁通过Lua脚本实现,并且支持可重入锁的特性,为了更加健全还需要进行监听实现自动续期。
但Redis如果集群,很难保证ACP特性,只能支持AP,其中一台RedisA机进行加锁,然后突然挂掉,从机B数据复制是异步的,从机B被升级为主,这个时候很难保证数据一致性,无法实现互斥的属性。
ZOOKEEPER集群,可以实现数据一致性,它可以保持CP,它底层必须主机和从机全部健康,并复制完成,才进行返回。不像Redis是异步的复制。
解决Redis分布式锁的单机故障:
需要部署多台Redis机器,保证机器数据为奇数,数量公司: N = 2X + 1 (N是最终部署机器数,X是容错机器数),并保证这些Redis都是独立的主机。
使用Redisson,底层通过Redlock算法实现的。
# 1.需要引入包:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.4</version>
</dependency>
# 2.使用
@Autowired private Redisson redisson;
RLock redissonLock = redisson.getLock("xxRedisLock");
// 加锁
redissonLock.lock();
// 解锁
if (redissonLock.isLocked() && redissonLock.isHeldByCurrentThread()){
redissonLock.unlock();
}
缓存淘汰
Redis内存慢了如何?
Redis默认的内存大小,在64位操作系统下,默认是0,表示不限制Redis内容使用;但在一般生产环境,我们设置物理内存的4分之3,如果设置内存大小,超过内存会爆OOM。
修改默认内存大小
// 方式1: 在redis配置文件里进行配置
maxmemory 104857600 (单位是byte)
// 方式2: 在redis执行命令行操作
config set maxmemory 104857600
// 查看
config get maxmemory
Redis过期key的删除策略
立即删除,对CPU不好,用时间换空间
惰性删除,对空间不好,用空间换时间
// 在redis配置文件进行 // 开启惰性删除, lazyfree-lazy-eviction=yes
定期删除,每隔一段时间进行删除过期的key,定期的key是抽查的,会有漏网之鱼的出现。
缓存淘汰策略:
// 一共8种策略
noevication : 不会删除任何key,如果内存满了会爆异常error
allkeys-lru: 对所有key使用LRU算法进行删除,优先删除掉最近不经常使用的key (推荐)
volatie-lru : 对所有设置了过期时间的key使用LRU算法删除
allkeys-random :对所有key随机删除
volatie-random :对所有设置了过期时间的key随机删除
volatie-ttl : 删除马上过期的key
allkeys-lfu: 对所有key使用LFU算法进行删除
volatile-lfu: 对所有设置了过期时间的key使用LFU算法进行删除
// 在redis配置文件里进行
maxmemory-policy noevication
IO多路复用
用一个进程来处理大量的用户连接,同时处理多个客户端连接。
Redis处理多并发客户端连接:
Redis使用epoll实现IO多路复用,将连续信息事件放到队列中,谁有任务处理,比如举手表示,就交给管理者进行分配,到任务执行返回给客户端。类似监考答卷现场。