redis-redis记录
重新捡起丢了六年的东西.
前篇
- Redis 菜鸟教程 - https://www.runoob.com/redis/redis-intro.html
- Redis从入门到精通,至少要看看这篇 (干货很多)
- Redis - https://wiki.shileizcc.com/confluence/display/RED/Redis
- redis如何防止并发? - https://wukong.toutiao.com/answer/6941718037494546692/
docker 安装
设置外网访问
- 注释
bind
, 并设置protected-mode no
bind
指定 ip (0.0.0.0 表示所有 ip)
protected-mode 的启用有两个条件, 1. 没有设置 bind
, 2. 没有设置密码访问
安全访问 - 设置密码
可以通过两种方式来达到安全的目的, 如果两者同时启用, 那么是最安全的
- 设置密码:
requirepass 123456
- 绑定 ip:
bind 127.0.0.1
方式一: 服务启动后 设置密码
方式二: 服务启动时 指定配置
- Linux下设置redis访问密码 - https://segmentfault.com/a/1190000016372447
配置文件启动 - 密码设置
- docker redis自定义配置文件启动 - https://www.jianshu.com/p/ff599cddc869
- 官方配置文件 - https://redis.io/topics/config, 从这里下载对应版的配置文件拿来修改
docker 启动 redis镜像里是没有配置文件的, 去官方下载修改, 需要修改的几个参数
一般来说我们需要修改的内容仅有下面几个
1
2
3
4
5
6
7
8
9
10# Redis默认不是以守护进程的方式运行,可以通过该配置项修改,使用yes启用守护进程
deamonize yes
# 你可以绑定单一接口,如果没有绑定,所有接口都会监听到来的连接, 0.0.0.0 表示任何ip都可以连
# bind 127.0.0.1
# 因为redis本身同步数据文件是按上面save条件来同步的,所以有的数据会在一段时间内只存在于内存中。默认为no
appendonly no
# 设置Redis连接密码,如果配置了连接密码,客户端在连接Redis时需要通过
# auth <password>命令提供密码,默认关闭,当前密码为123456
requirepass 123456
pidfile /var/run/redis_6379.pid # 如果有多个 Redis 实例, 需要改成对应的端口启动指定配置文件
1
2
3
4
5docker run -d -p 6379:6379 --name redis3
-v /root/redis/redis.conf:/etc/redis/redis.conf
-v root/redis/data:/data
hub.c.163.com/library/redis:latest
redis-server /etc/redis/redis.conf --appendonly yes将本地配置文件映射到镜像中,这样更改本地文件就可以直接修改镜像中的配置了,更改之后使用docker restart+容器ID 重启服务。
/docker/redis/redis.conf
是宿主机子上的文件,/etc/redis/redis.conf
是容器里面的文件
启动时最后也可以加配置参数“–appendonly yes”
配置文件
ocker 启动 redis镜像里是没有配置文件的, 去官方下载修改, 需要修改的几个参数
一般来说我们需要修改的内容仅有下面几个
1 | # Redis默认不是以守护进程的方式运行,可以通过该配置项修改,使用yes启用守护进程 |
主从配置
配置文件方式
命令方式
Redis 集群演变过程
单机版
主从复制
主: 读写, 从: 读.
缺点: 写 无法负载均衡, 故障恢复无法自动化
哨兵
在复制的基础上, 实现了自动化故障恢复
缺点: 写 无法负载均衡, 故障转移 (选取主节点) 期间 服务不可用
集群
解决了 写 无法负载均衡 等以上缺点, 高可用.
Redis 是二进制存储
验证: 设置一个 abc
, 其二进制是 1100001 1100010 1100011
1 | redis:6379> set wilker abc |
位图
位数组的顺序 和 字符的位顺序 是相反的
1 | 127.0.0.1:6379> setbit s 1 1 |
单机版
集群
可以理解为 多组哨兵, 这里 是 3 组, 也就是 3 个 分片, Redis 1000 组以内性能不会有很大影响
每一组 都是 一主多从, 数据都是一样的, 但是组与组之间是无关系的, 数据不是一致的
对 hello 计算 hash 值, 取模分配到其中一个 组 中.
缓存几大问题
- 缓存穿透,缓存击穿,缓存雪崩解决方案分析 - https://blog.csdn.net/zeb_perfect/article/details/54135506
- Redis缓存雪崩、缓存穿透、热点Key解决方案和分析 - https://blog.csdn.net/wang0112233/article/details/79558612
缓存穿透
是指数据控当中没有的数据, 缓存也没有的数据, 导致一直会查询数据库, 没有起到 减少数据库查询 的目的, 数据库压力过大
解决方案:
- 将空值缓存起来. 简单粗暴的方法, 如果一个查询返回的数据为空(不管是数 据不存在,还是系统故障),我们仍然把这个空结果进行缓存,设置比较短的过期时间, 如 几十秒
- 使用 布隆过滤器, 可以确定某个数据 肯定不存在, 这种情况就不需要去查数据库, 但已足够过滤大部分不合法攻击; 但无法确定 肯定存在, 这种情况还需要去查 数据库.
缓存击穿
是指缓存中没有但数据库中有的数据 (一般是缓存时间到期), 这是由于并发用户特别多, 同时读缓存没有读到数据, 有同时去数据库取数据, 引起数据库压力瞬间增大, 造成过大压力.
解决方案:
- 互斥锁
缓存雪崩
是指缓存服务器重启或者大量缓存集中在某一个时间段内失效
解决方案:
- 主要就是要搭建高可用集群, 保证机器的高可用
- 对不同的数据使用不同的失效时间, 甚至对相同的数据, 不同的请求使用不同的失效时间.
缓存与数据库一致性
解决方案:
先删除缓存, 再修改数据库. 如果数据库修改失败了, 那么数据库中依旧是 旧数据, 缓存中中空的, 那么数据不会不一致. 因为读的时候缓存没有, 则读数据库中的旧数据, 然后更新到缓存中.
如果更新数据库的过程中又有其他线程查询更新缓存, 那么还是有数据不一致问题.
解决办法:
延时双删. 也就是更新完数据库后再删一次缓存, 此时删除缓存要延时一下.
- 串行化. 也就是使用队列
使用缓存的思路
先查询 缓存, 有则返回; 没有则查询 数据库, 查询到的数据加入到 缓存 中
布隆过滤器
- 布隆过滤器go实现 - https://blog.csdn.net/liuzhijun301/article/details/83040178
- 官网推荐了 RedisBloom 作为 Redis 布隆过滤器的 Module,地址: https://github.com/RedisBloom/RedisBloom
- Redis 布隆过滤器 - https://wiki.shileizcc.com/confluence/pages/viewpage.action?pageId=53477553
- Redis 布隆过滤器实战「缓存击穿、雪崩效应」 - https://juejin.im/post/5c9442ae5188252d77392241
布隆过滤器一般用来判断一个数据是否在一个很大的数据集合里面。当然可以用数组,集合,树等数据结构和各种查找法都可以做同样的事情,但是布隆过滤器有更好的时间效率和空间效率。比特币实现SPV节点时使用了布隆过滤器来查询交易。布隆过滤器可以判断一个数在不在集合里,但存在一定的误判率。
布隆过滤器的核心是一个超大的位数组和几个哈希函数。假设位数组的长度为m,哈希函数的个数为k。
以上图为例,在这里维数组长度为18,哈希函数个数为3个。首先将维数组所有位全部置0。集合中有的3个数据x,y,z,通过3个哈希函数对每一个数据进行计算,得到该数据的哈希值,这个哈希值对应维数组上面的一个点,然后将对应位数组的位置1。这样3个数据会生成9个点。对于另外一个数据w,查询它 在不在集合中的方法是对w通过3个哈希函数映射到位数组上,判断3个映射位置是否为1。只要有一个位置为0,就能说明w一定不在集合中。反之如果3个点都为1,则说明这个元素可能在集合中。此处不能判断元素一定在集合中,因为存在一定的误判率。比如对于上图中的4,5,6这3个位置都为1,但是它是不同的数据映射到的点。如果有一个数据刚好映射到这3个位置,虽然它不在集合中,但是我们也会误判它。
添加元素
- 将要添加的元素给k个哈希函数进行计算
- 得到位于位数组上面的k个位置
- 将位数组上对应位置1
查询元素
- 将要查询的元素给k个哈希函数
- 得到对应于位数组上的k个位置
- 如果k个位置有一个为0,则肯定不在集合中
- 如果k个位置全部为1,则可能在集合中
比如判断某个 用户名 是否存在, 可以在启动服务的时候, 把数据库中的 用户名 全 load 出来, 通过 布隆过滤 算法丢到 Redis 的某个 key 中 (bit), 查询的时候通过 布隆过滤器 查询是否存在这个用户.
特点: bloom filter 只能添加, 不能删除. 因为 算出来的 哈希值取模 后的数组下标可能会 碰撞, 然后将值置为 1, 如果删除的话所有 碰撞 在一起的都删除了, 所以只能加, 不能减.
Redis 中集成 redisbloom
- redisbloom - https://github.com/RedisBloom/RedisBloom
- Redis05——Redis高级运用(管道连接,发布订阅,布隆过滤器) - http://www.manongzj.com/blog/5-cmatmzbxmpbmbqd.html
持久化
Redis 提供了 两种持久化方案, 默认开启 RDB
RDB
Redis 会 fork 一个与当前进程一模一样的子进程来进行持久化, 这个子进程的所有数据 都与 原进程一模一样, 会先将数据写入到一个临时文件中, 待持久化结束了, 再用这个临时文件替换上一次持久化好的文件, 整个过程中, 原进程不进行任何的 io 操作, 这就确保了极高的性能.
持久化文件在那? 触发时机
shutdown 时, 如果没有开启 aof, 会触发
配置文件中默认的快照配置
执行命令 save 或者 bgsave, save 是直观保存, 其他不管, 全部阻塞, bgsave 是 Redis 会 fork 子进程 后台异步进行快照操作, 同时可以响应客户端的请求
适合大规模数据恢复, 对完整性和一致性不高, 在一定间隔时间做一次备份, 会丢失备份前突然宕机的数据.
AOF
将 Redis 的操作日志以追加的方式写入到文件, 读操作是不记录的
持久化文件在那? 触发时机
no : 表示等操作系统惊醒数据缓存同步到磁盘, (快, 持久化没保证)
always : 同步持久化, 每次发生数据变更时, 立刻记录到磁盘 (慢, 安全)
everysec : 表示每秒同步一次 (默认值, 很快, 但可能丢失 1s 以内的数据)
官方建议 两种持久化同时使用, 如果两个同时启用, 优先使用 aof 持久化.
性能建议:
rdb 制作后备用途, save 900 1
就够了, 也就是 15分钟备份一次
aof 方式 auto-aof-rewrite-min-size 64mb
, 在生产环境中, 这个值一般都修改为 5gb 以上, 减少重写次数, 提高性能
命令
SET
SET key value [EX seconds][PX milliseconds][NX|XX]
- EX seconds:设置指定的过期时间,单位秒。
- PX milliseconds:设置指定的过期时间,单位毫秒。
- NX:仅当key不存在时设置值。
- XX:仅当key存在时设置值。
可以看出来,SET
命令的天然原子性完全可以取代 SETNX
和 EXPIRE
命令。
获取有 key
使用模糊查找所有 key
1 | redis:6379> KEYS * |
go-redis使用
- gomodule (star 比较多的)
- Golang操作redis指南 - https://www.jianshu.com/p/89ca34b84101
- golang-redis之hash类型简单操作 - https://studygolang.com/articles/12285
- Go实战–golang中使用redis(redigo和go-redis/redis) - https://blog.csdn.net/wangshubo1989/article/details/75050024
分布式锁
- Redis分布式锁的正确实现姿势 - https://benjaminwhx.com/2018/08/26/Redis分布式锁的正确实现姿势/
- 使用 Redis 实现分布式锁的正确姿势 - https://blog.didiyun.com/index.php/2019/01/14/redis-3/
- golang 实现 - https://github.com/yangxuan0261/GoLab/blob/master/test_db/test_redis/redis_action02_lock_test.go
上锁: 可以用 SET 命令的 NX 参数, 还在使用 2.6.12
版本之前的同学只能使用另一法宝:Lua脚本来保证原子性了。
解锁: 使用lua
事务
单个 Redis 命令的执行是原子性的,但 Redis 没有在事务上增加任何维持原子性的机制,所以 Redis 事务的执行并不是原子性的。
事务可以理解为一个打包的批量执行脚本,但批量指令并非原子化的操作,中间某条指令的失败不会导致前面已做指令的回滚,也不会造成后续的指令不做。
原子性
设计到多个命令要想保持原子性必须得使用 Lua 脚本。
比如 分布式锁
加锁
Redis 在
2.6.12
版本开始,为SET
命令增加了一系列选项, 可以确保原子性:1
SET key value [EX seconds][PX milliseconds][NX|XX]
还在使用
2.6.12
版本之前的同学只能使用另一法宝:Lua脚本来保证原子性了。1
2
3
4
5
6
7
8
9
10public static boolean tryLock(String key, String uniqueId, int seconds) {
String luaScript = "if redis.call('setnx', KEYS[1], KEYS[2]) == 1 then " +
"redis.call('expire', KEYS[1], KEYS[3]) return 1 else return 0 end";
List<String> keys = new ArrayList<>();
keys.add(key);
keys.add(uniqueId);
keys.add(String.valueOf(seconds));
Object result = jedis.eval(luaScript, keys, new ArrayList<String>());
return result.equals(1L);
}
解锁
1
2
3
4
5
6// 释放分布式锁
public static boolean releaseLock(String key, String uniqueId) {
String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) else return 0 end";
return jedis.eval(luaScript, Collections.singletonList(key), Collections.singletonList(uniqueId)).equals(1L);
}