1. redis是什么
Redis:是一个nosql的数据库,它提供了多种数据类型的内存数据库,用来解决我们不同的业务问题。
作用:
- 在程序中缓冲使用。
- 处理大数据高并发的时候使用。
2. Redis在Linux下的安装
- 通过Docker安装,命令是:docker run -p 6370:6379 -d redis
- 进入Docker安装目录: docker exec -it 容器ID /bin/bash
- 打开redis客户端:redis-cli
- 添加值set 变量名 值 例如:set name sss
- 获取值get 变量名 例如:get name
- 退出exit
- 在windows cmd下启动命令:redis-server redis.windows.conf
- 在windows cmd下启动命令:redis-cli.exe -h 127.0.0.1 -p 6380
3. redis 与传统数据库的比较
优点:
- 数据储存在内存中,没有传统数据库的IO读写能大幅提升程序对数据的读写速度。
- 可以多路复用,即把监听和阻塞等待请求由系统内核完成。
4. 多路复用
5.连接Redis 的组件
生产环境中使用的是:ServiceStack.Redis。需要收费限制使用6000 次但是可以下载源码破解重新编译使用。
破解方式:
步骤1:https://github.com/ServiceStack/ServiceStack.Text 下载源码找到LicenseUtils类,修改一下内容
public static void ApprovedUsage(LicenseFeature licenseFeature, LicenseFeature requestedFeature,
int allowedUsage, int actualUsage, string message)
{
return;//新增return
var hasFeature = (requestedFeature & licenseFeature) == requestedFeature;
if (hasFeature)
return;
if (actualUsage > allowedUsage)
throw new LicenseException(message.Fmt(allowedUsage)).Trace();
}
编译生成 ServiceStack.Text.dll,在.netstandard2.0目录
步骤2:在所建立建立的项目中 nuget ServiceStack.Redis 。
应用后会下载 Microsoft.Bcl.AsyncInterfaces.dll ,ServiceStack.Common.dll,ServiceStack.Interfaces.dll,ServiceStack.Redis.dll,ServiceStack.Text.dll 把最后的这个 DLL 用步骤1 生成的替换掉。
步骤3:项目去掉 nuget ServiceStack.Redis 。单独的应用现在的的5个DLL。
6.Redis的10大数据结构
1. String 类型
保存的数据就是键值对(key ,value);
using(RedisClient clent = new RedisClent("127.0.0.1" ,6379))
{
//函数1. 删除当前数据库的数据
clent.FlushDb();
//函数2 删除所有数据库的数据
clent.FlushAll();
//函数3 保存数据
clent.Set<string>("Name","xiaoshizi");
//函数4 获取数据(错误的方式) 结果就是 "xiaoshizi";
clent.GetValue("Name");
//正确的方式 结果就是 xiaoshizi
JsonConvert.DeserializeObject<string>(clent.GetValue("Name"));
//函数5 正确的方式 结果就是 xiaoshizi 本质也是做了序列化的
clent.Get<string>("Name");
//函数6 设置过期时间 一秒以后过期 也可以设置具体的日期
clent.Set<String>("Name","xiaoshizi",TimeSpan.FromSeconds(1));
//函数7 批量的添加值
client.SetAll(new Dictionary<string, string> { { "id", "001" }, { "name", "clay" } });
//函数8 批量获取值 批量读取内存中多个key的结果 如果我们获取的key不存在,程序会返回一个空的字符串
IDictionary<string, string> getall = client.GetAll<string>(new string[] { "id", "name", "number" });
//函数9 原有的值后面追加值
client.AppendToValue("name", "I");
//函数10 获取旧值设置新值
var value = client.GetAndSetValue("name", "clay");
//函数11 自增 从0开始,调用一次增加一次 ,注意不论原来保存的是多少数据从0 开始
client.Incr("sid");
//函数12 自增 在原有的基础上 + 100
client.IncrBy("sid", 100);
//函数13 自减 从0开始,调用一次减少一次 ,注意不论原来保存的是多少数据从0 开始
client.Decr("sid"):
//函数14 自减 在原有的基础上 - 100
client.Decr("sid", 100);
}
注意:
1:数据的保存是做了序列化进行保存的.
2.使用场景
1.分布式保存Session
2.自增自减做网站请求的数量,评论的数量
3.add和set 的区别
1.Add 只做新增,如果已经有了则返回:fasle
2.Set 如果有key就替换,如果没有就写入
4.数据的保存,应该尽量的不要使用String 类型,如果保存数据大于44个字节 保存类型就是raw 类型。 raw类型的数据redis会申请多余的空间出来,从而造成了内存的浪费。保存数据小于44个字节 redis会自动的选择embstr 的累心保存,不浪费内存空间,如果是int型的数据 ,redis申请的就是int型的内存空间
2. Hash类型
保存的数据为hastable
using (RedisClient client = new RedisClient("81.70.91.63", 6370))
{
string hashid = "shizhanwen";
//函数1 添加一个行
client.SetEntryInHash(hashid, "id", "001");
//函数2 获取一行的值
client.GetValuesFromHash(hashid, "id").FirstOrDefault();
client.SetEntryInHash(hashid, "name", "clay");
client.SetEntryInHash(hashid, "socre", "100");
//函数3 批量增加
Dictionary<string, string> pairs = new Dictionary<string, string>();
pairs.Add("id", "001");
pairs.Add("name", "clay");
client.SetRangeInHash(hashid, pairs);
//函数4 批量获取
List<string> list = client.GetValuesFromHash(hashid, "id", "name", "abc");
List<string> list1 = client.GetAllEntriesFromHash(hashid);
//函数5 如果hashid集合中存在key/value则不添加返回false,如果不存在在添加key/value,返回true
client.SetEntryInHashIfNotExists(hashid, "name", "你好美");
//函数6 添加类 获取和修改同用 ,如果ID 一样就是可以修改
client.StoreAsHash<User>(new User() { Id = 1, Name = "ss", addr = 0 });
//函数7 获取类
client.GetFromHash<User>(1);
//函数8 删除
client.RemoveEntryFromHash(hashid, "id");
//函数9 判断是不是存在
client.HashContainsEntry(hashid, "id");
client.HashContainsEntry(hashid, "name");
//函数10 但是必须是数字
client.IncrementValueInHash(hashid, "socre", 2)
}
注意:
1:底层的数据结构是ZipList 和Has
1:ZipList:是个压缩版的List
1.ZIPList是通过保存数据的大小来开辟空间的同时也存在两个问题:1.每次新增数据都的开辟新的空间并且做大数据的额拷贝。2.由于保存数据的大小是不确定的不能用索引查询数据,查询数据费时间。
2:Has:存在两个问题:1.数据扩容问题,存在两个Hastable ,一个是有长度的一个是没有长度的,扩容的时候就把有长度的拷贝到没有长度的地方。2:拷贝数据的时候就存在了数据迁移的问题,它做的不是一下把所有数据迁移过去,而是通过操作一次数据做一点迁移,同时也会开启一个后台的任务做数据的迁移。
2:类中必须有ID 不分大小。
3:ID 中添加“:”可以在redis可视化工具中的数据库看到自建的文件夹。
3. List类型
string listid = "list";
var shishi = new UserInfo() { Id = 1, Name = "shishi" };
//函数1 添加
client.AddItemToList(listid, JsonConvert.SerializeObject(shishi));
//函数2 追加 向右边
client.PushItemToList(listid, JsonConvert.SerializeObject(shishi));
//函数3 追加 向左边
client.PrependItemToList(listid, JsonConvert.SerializeObject(shishi));
//函数4 设置过期时间 1 秒后过期 整个集合设置不是单独的一个元素
client.ExpireEntryAt(listid, DateTime.Now.AddSeconds(1));
//函数5 批量插入
client.AddRangeToList(listid, new List<string>() { "001", "002", "003", "004" });
//函数6 批量读取
client.GetAllItemsFromList(listid);
//函数7 获取下表从0 - 1 的数据
client.GetRangeFromList(listid, 0, 1);
//函数8 移除尾部数据并返回移除的数据
client.RemoveEndFromList(listid);
//函数9 移除头部数据并返回移除数据
client.RemoveStartFromList(listid);
//函数10 移除尾部的数据并添加到新的list 的头部
client.PopAndPushItemBetweenLists(listid, "newlist");
//函数11 获取指定位置的数据
client.GetItemFromList("newlist",0);
//函数12 修改当前下标的数据
/client.SetItemInList(listid, 0, "new value");
注意:
1.Redis6.0 现在的List不可以加入相同的值了
4. Set类型
Set也是一个集合,不过是一个去重的集合
string key = "set";
//函数1 批量的增加值
client.AddRangeToSet(key, new List<string>() { "001", "001", "002", "003", "003", "004" });
//函数2 随机的获取值
client.GetRandomItemFromSet(key);
//函数3 批量的获取值
client.GetAllItemsFromSet(key);
//函数4 随机的删除一个值
client.PopItemFromSet(key);
//函数5 删除一个值
client.AddRangeToSet(key, new List<string>() { "001", "001", "002" });
client.RemoveItemFromSet(key, "001");
//函数6 从一个集合中的值删除添加到另一个集合中
client.AddRangeToSet("fromkey", new List<string>() { "003", "001", "002", "004" });
client.AddRangeToSet("tokey", new List<string>() { "001", "002" });
//从fromkey 中把元素004 剪切到tokey 集合中去
client.MoveBetweenSets("fromkey", "tokey", "004");
//函数7 并集
client.AddRangeToSet("keyone", new List<string>() { "001", "002", "003", "004" });
client.AddRangeToSet("keytwo", new List<string>() { "001", "002", "005" });
var unionlist = client.GetUnionFromSets("keyone", "keytwo");
//把 keyone 和keytwo 并集结果存放到newkey 集合中
client.StoreUnionFromSets("newkey", "keyone", "keytwo");
//函数8 交集
client.AddRangeToSet("keyone", new List<string>() { "001", "002", "003", "004" });
client.AddRangeToSet("keytwo", new List<string>() { "001", "002", "005" });
var Intersectlist = client.GetIntersectFromSets("keyone", "keytwo");
//把 keyone 和keytwo 交集结果存放到newkey 集合中
client.StoreIntersectFromSets("newkey", "keyone", "keytwo");
<font color="green">注意:</font>
<font color="green">1.应用场景:投票 ,取交集,取并集 好友推荐</font>
5. Zset类型
Set也是一个集合,不过是一个去重的集合,具备Set 的所有功能 另外加了分数。自动的排序。
数据结构就是跳跃表:内容放到 Has ,分数放到跳跃表
string zsett_key = "zset";
//函数1 添加一个kye 如果有相同的值,则会替换(覆盖)进去,不会因为分数保留
client.AddItemToSortedSet(zsett_key, "cc", 33);
client.AddItemToSortedSet(zsett_key, "cc", 44);
//函数2 批量添加
client.AddRangeToSortedSet(zsett_key, new List<string>() { "a", "b" }, 1);
//函数3 批量获取
client.GetAllItemsFromSortedSet(zsett_key);
//函数4 获取下标的值
client.AddItemToSortedSet("蜀国", "刘备", 5);
client.AddItemToSortedSet("蜀国", "关羽", 2);
client.AddItemToSortedSet("蜀国", "张飞", 3);
client.AddItemToSortedSet("魏国", "刘备", 5);
client.AddItemToSortedSet("魏国", "关羽", 2);
client.AddItemToSortedSet("蜀国", "张飞", 3);
client.GetRangeWithScoresFromSortedSet("蜀国", 0, 2);
//函数5 将参数2 ,参数3 的集合放到 Zset 2中
client.StoreIntersectFromSortedSets("2", "蜀国", "魏国");
注意:
1.应用场景:加了权重 ,抽奖,限流,排行榜.
6. BitMaps类型
是一个节省内存的数据结构。里面是0或1.
.Net 没有对它做封装。
7.Hyperlogloss类型
基准统计数据结构类型,使用少量的内存,保存大量的数据。不足就是:有误差
案例:投票,网页浏览统计,
8.Streams类型
做发布订阅的
9.Geo类型
是一个地理位置,不同的Key储存的是是经度和维度,可计算两点之间的距离。
10.Bloom Filter类型
1. 说明
布隆过滤器:缓存预热,缓存雪崩,缓存穿透里面用的比较多。它是一个数据结构和算法,可以把它直接安装到Redis中的插件。
过滤器:
- 储存数据,占用很少的内存保存好多的数据。
- 判断数据在不在储存里面,查询效率的问题。
2. 原理
(一个bit -> 8个位 = 一个字节)
建立一个bit[]
对要保存的数据做不同的Hash 然后保存到bit数组中。
注意:
通过布隆过滤器判断Key在不在里面,存在误判的可能性。
bit数组越长,hash的方法越多,则占用的内存越多,但是误判越小,反之 ,数组越短,hash方法越少,则占用内存越少,误判概率越大。
3. 安装
bloom-filter · GitHub Topics · GitHub
docker run -p 6379:6379 --name redis-redisbloom redislabs/rebloom:latest
- 保存 bf.add 仓库名称 保存的值 例如:bf.add myfilter a
- 判断 bf.exists 仓库名称 保存的值 例如:bf.exists myfilter a
//代码实现 使用的是 ServiceStack.Redis
client.Custom("bf.add", "myfilter", "dddd");
client.Custom("bf.exists", "myfilter", "dddd4444").Text;
//同时也可以执行命令
client.Custom("set", "name", "a");
7.事务
1. Redi事务
Redis事务是没有回滚的。
具有ACID:
原子性:最小单位。
一致性:多条命令同时成功,同时失败。
隔离性:一个会话和另一个会话是隔离的
持久性:执行后,数据保存到磁盘
//1.普通的事务是没有锁的,在执行过程中可以由别的进程修改值.
using (RedisClient client = new RedisClient("127.0.0.1", 6379))
{
using (var trans = client.CreateTransaction())
{
trans.QueueCommand(p => p.Set("a", "3"));
trans.QueueCommand(p => p.Set("b", "3"));
trans.QueueCommand(p => p.Set("c", "3"));
var flag = trans.Commit();
}
}
//2.如果加上监听就可以达到正在的事务效果,在提交的过程中会失败。
using (RedisClient client = new RedisClient("127.0.0.1", 6379))
{
client.Watch("c","a","c");
using (var trans = client.CreateTransaction())
{
trans.QueueCommand(p => p.Set("a", "3"));
trans.QueueCommand(p => p.Set("b", "3"));
trans.QueueCommand(p => p.Set("c", "3"));
var flag = trans.Commit();
}
}
2. Lua
Lua是一种轻量级的脚本语言,用标准C语言编写并以源代码形式开放,其设计的目的是为了嵌入到应用程序中,从而为应用程序提供灵活的扩展和定制功能。
可以把lua理解成数据存储过程,但是实质不一样,而且lua具有原子性,而且性能要比我们直接操作指令性能要高---lua是c语言编写的,redis也是c语言编写,不存在编译过程。
client.ExecLuaAsString(@"redis.call('set','number','10')");
var lua = @"local count = redis.call('get',KEYS[1])
if(tonumber(count)>=0)
then
redis.call('INCR',ARGV[1])
return redis.call('DECR',KEYS[1])
else
return -1
end";
client.ExecLuaAsString(lua, keys: new[] { "number" }, args: new[] { "ordercount" });
Lua 脚本参数从下标1开始。
8.持久化
把数据以文件的方式保存到硬盘。当服务器宕机的时候可以把数据从硬盘加载回来。
现在redis的持久化方式分两种
1. RDB 文件(快照)
1. 快照方式
每次全量复制,备份的策略是
save 900 1 900 秒(15 分钟)内有 1 个更改
save 300 10 300 秒(5 分钟)内有 10 个更改
save 60 10000 60 秒内有 10000 个更改
2. 特点
缺点:一般是默认不用修改,这种情况服务器宕机会导致数据的丢失。
优点:备份缓慢,但是重启加载数据性能比较快
3. 手动备份策略
-
Save:阻塞线程--- 因为执行这个指令的线程就是redis里面唯一的那个执行指令的线程-
当你备份大量的数据的时候,如果耗时比较长,则当有其他客户端发送指令的时候,会卡主。
-
bgsave:单独的线程,后台专门有一个线程去实现备份。
这个后台的就是专门来根据我们的配置文件里策略然后去备份数据文件。底层原理: 有一个定时器,就是不停的计算我们阈值。每一次bgsave之后,要把计数器清0
2. AOF文件(文件追加)
1.介绍
默认是不开启的:在配置文件中 appendonly no 改为 appendonly yes
用配置文件启动 :redis-server.exe redis.windows.conf
命令查看:config get appendonly
日志追加是顺序的读写。
2. 三种追加方式:
类型 | 安全性 | 性能 |
---|---|---|
NO(业务不忙时追加,不推荐) | 低 | 高 |
everysec(每一秒钟保存,推荐) | 比较高 | 比较高 |
always(只要有读写) | 高 | 低 |
配置文件中默认方式 :appendfsync everysec
命令查看:config get appendfsync
3. aof文件的保存说明:
*参数的数量
$参数的长度
参数值
所以可以修改文件中的值
4. 混合模式
aof优点是:文件可读行高,每次都是追加修改的东西,备份比较快,但是当服务重启的时候,加载数据的时候比较慢。
如果rdb和aof都开启了,则默认首先加载aof文件,为了保证数据的尽可能完整性
aof 重写问题:当数据文件到达一个阈值的时候,我们的文件会继续重写变成一个小文件。由原来的竖列变成了列表的横列。
混合模式(redis 4.X版本之后): rdb(备份慢,启动快),aof(备份快,启动慢)开始aof默认模式就是 混合模式开启,通过aof备份,rdb模式启动,设计原理为:后台有个专门的任务:(使用aof重写)进行aof操作的时候,把aof文件变成rdb,然后之后的操作直接取做日志追加。混合模式的文件包含aof与rdb。
以下是混合模式的文件示意图:
命令:bgrewriteaof 就是重写把以前的aof格式改成rdb模式,后面的继续用aof保存。
9.主从架构
主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器,主从是哨兵和集群模式能够实施的基础。前者称为主节点(master),后者称为从节点(slave),数据的复制是单向的,只能由主节点到从节点。
- 主从架构,我们操作的是主节点,当主节点有数据的时候主节点会把数据发送到从节点。
- 当主从节点第一次实现主从架构的时候,主节点会做一个全量备份,主节点会把整个RDB文件发送到从节点。之后就是追加。
- 可以做到读写分离,读的时候操作从节点,写的时候操作主节点
- 当主节点宕机了,需要改代码。或者用keeplive 搭建热备。
-
一主一从
-
一主多从
-
树状主从
1. 主从搭建
1. 命令模式
在从服务器执行下面的命令成为或取消成为某节点的从节点
#slaveof 主服务器的IP 端口号
创建主从:slaveof host port
取消主从:slaveof no one
查看主从:info replication
从服务器配置只读 :config set slave-read-only yes
2. 配置文件模式
在从节点配置文件中新增下面两个配置即可指定成为某个主节点的从节点
slaveof 主节点地址 主节点端口
slaveof host port
从服务器只读(推荐配置)
slave-read-only yes
注意:
1.一般情况下,使用主从的时候,防止数据不一致,从节点只能读不写。
2.执行slaveof 命令的时候 从服务器的数据会被清空,然后才同步主服务器的数据。
3.主节点宕机了,从节点可以替换主节点,不是自动替换,需要程序员修改代码,改ip地址和端口,或者应该是用keeplive 用vip地址来高可用,这样的主从是不会自动切换的。
3.ServiceStack.Redis还不支持集群。
2. 哨兵模式
1. 介绍
哨兵:用于对主从架构中的每一台服务器进行监控,当主节点出现故障后通过投票机制来挑选新的主节点,并且将所有的所有的从节点连接到新的主节点上。
监控:监控主从节点的运行情况
通知:当监控节点出现故障,哨兵之间进行通讯。
自动故障转移:当监控到主节点宕机后,断开与宕机节点的连接的所有从节点,然后在从节点中选取一个作为主节点,将其他的从节点连接到这个新的主节点上,最后通知客户端最新的服务器地址。
以下是哨兵模式架构图:
2. 宕机情况
主观宕机:单独哨兵认为你宕机了,发现了故障。
客观宕机:半数哨兵认为主节点宕机,发现了故障。
客观的宕机才会导致主节点的切换。
3. 选举主节点的原则
健康度:从节点响应的时间,比如心跳时间的长短。
完整性:根据我们从节点备份的完整性,根据数据备份偏移量,偏移量越大完整性越高。
稳定性:根据启动时间周期,心跳检测 ,启动时间越早。
如果上面三个条件都相等,则根据我们节点启动时分配的runid决定,如果runid 越小,则最有可能选择为我们主节点。
选举原理:让最先发现主机宕机的节点哨兵负责做leader(之前是没有leader的),通过选举节点的原则选出主节点,并做主节点的切换。
4. 故障转移
哨兵Leader 根据一定规则从各个从节点中选择出一个节点升级为主节点。
其余从节点修改对应的主节点为新的主节点。
当原主节点恢复启动的时候,变为新的主节点的从节点。
5. 搭建
1. 哨兵搭建:
mkdir /usr/local/etc/redis/sentinel/sentinel -p
cd /usr/local/etc/redis/sentinel/sentinel
redis-sentinel-1.conf
# bind 127.0.0.1
# 哨兵的端口号
# 因为各个哨兵节点会运行在单独的Docker容器中
# 所以无需担心端口重复使用
# 如果需要在单机
port 26379
# 设定密码认证
requirepass 123456
# 配置哨兵的监控参数
# 格式:sentinel monitor <master-name> <ip> <redis-port> <quorum>
# master-name是为这个被监控的master起的名字
# ip是被监控的master的IP或主机名。因为Docker容器之间可以使用容器名访问,所以这里写master节点的容器名
# redis-port是被监控节点所监听的端口号
# quorom设定了当几个哨兵判定这个节点失效后,才认为这个节点真的失效了
sentinel monitor local-master 127.0.0.1 6379 2
# 连接主节点的密码
# 格式:sentinel auth-pass <master-name> <password>
sentinel auth-pass local-master 123456
# master在连续多长时间无法响应PING指令后,就会主观判定节点下线,默认是30秒
# 格式:sentinel down-after-milliseconds <master-name> <milliseconds>
sentinel down-after-milliseconds local-master 30000
redis-sentinel-2.conf
port 26380
requirepass 123456
sentinel monitor local-master 127.0.0.1 6379 2
sentinel auth-pass local-master 123456
# master在连续多长时间无法响应PING指令后,就会主观判定节点下线,默认是30秒
# 格式:sentinel down-after-milliseconds <master-name> <milliseconds>
sentinel down-after-milliseconds local-master 30000
redis-sentinel-3.conf
port 26381
requirepass 123456
sentinel monitor local-master 127.0.0.1 6379 2
sentinel auth-pass local-master 123456
# master在连续多长时间无法响应PING指令后,就会主观判定节点下线,默认是30秒
# 格式:sentinel down-after-milliseconds <master-name> <milliseconds>
sentinel down-after-milliseconds local-master 30000
docker-compose.yam
cd /usr/local/etc/redis/sentinel/sentinel
vi docker-compose.yml
version: '3'
services:
redis-sentinel-1:
image: redis
container_name: redis-sentinel-1
restart: always
# 为了规避Docker中端口映射可能带来的问题
# 这里选择使用host网络
network_mode: host
volumes:
- ./redis-sentinel-1.conf:/usr/local/etc/redis/redis-sentinel.conf
# 指定时区,保证容器内时间正确
environment:
TZ: "Asia/Shanghai"
command: ["redis-sentinel", "/usr/local/etc/redis/redis-sentinel.conf"]
redis-sentinel-2:
image: redis
container_name: redis-sentinel-2
restart: always
network_mode: host
volumes:
- ./redis-sentinel-2.conf:/usr/local/etc/redis/redis-sentinel.conf
environment:
TZ: "Asia/Shanghai"
command: ["redis-sentinel", "/usr/local/etc/redis/redis-sentinel.conf"]
redis-sentinel-3:
image: redis
container_name: redis-sentinel-3
restart: always
network_mode: host
volumes:
- ./redis-sentinel-3.conf:/usr/local/etc/redis/redis-sentinel.conf
environment:
TZ: "Asia/Shanghai"
command: ["redis-sentinel", "/usr/local/etc/redis/redis-sentinel.conf"]
2. redis 配置
mkdir /usr/local/etc/redis/sentinel/server -p
cd /usr/local/etc/redis/sentinel/server
redis-master.conf
# 监听端口
port 6379
# 启动时不打印logo
# 这个不重要,想看logo就打开它
always-show-logo no
# 设定密码认证
requirepass 123456
# 禁用KEYS命令
# 一方面 KEYS * 命令可以列出所有的键,会影响数据安全
# 另一方面 KEYS 命令会阻塞数据库,在数据库中存储了大量数据时,该命令会消耗很长时间
# 期间对Redis的访问也会被阻塞,而当锁释放的一瞬间,大量请求涌入Redis,会造成Redis直接崩溃
rename-command KEYS ""
# 设定连接主节点所使用的密码
masterauth "123456"
redis-slave1.conf
# bind 127.0.0.1
# 监听端口
port 6380
always-show-logo no
requirepass 123456
rename-command KEYS ""
slaveof 127.0.0.1 6379
# 设定连接主节点所使用的密码
masterauth "123456"
redis-slave2.conf
# 监听端口
port 6381
always-show-logo no
# 设定密码认证
requirepass 123456
rename-command KEYS ""
slaveof 127.0.0.1 6379
# 设定连接主节点所使用的密码
masterauth "123456"
docker-compose.yam
cd /usr/local/etc/redis/sentinel/server
vi docker-compose.yml
version: '3'
services:
# 主节点的容器
redis-server-master:
image: redis
container_name: redis-server-master
restart: always
# 为了规避Docker中端口映射可能带来的问题
# 这里选择使用host网络
network_mode: host
# 指定时区,保证容器内时间正确
environment:
TZ: "Asia/Shanghai"
volumes:
# 映射配置文件和数据目录
- ./redis-master.conf:/usr/local/etc/redis/redis.conf
- ./data/redis-master:/data
command: ["redis-server", "/usr/local/etc/redis/redis.conf"]
# 从节点1的容器
redis-server-slave-1:
image: redis
container_name: redis-server-slave-1
restart: always
network_mode: host
depends_on:
- redis-server-master
environment:
TZ: "Asia/Shanghai"
volumes:
- ./redis-slave1.conf:/usr/local/etc/redis/redis.conf
- ./data/redis-slave-1:/data
command: ["redis-server", "/usr/local/etc/redis/redis.conf"]
# 从节点2的容器
redis-server-slave-2:
image: redis
container_name: redis-server-slave-2
restart: always
network_mode: host
depends_on:
- redis-server-master
environment:
TZ: "Asia/Shanghai"
volumes:
- ./redis-slave2.conf:/usr/local/etc/redis/redis.conf
- ./data/redis-slave-2:/data
command: ["redis-server", "/usr/local/etc/redis/redis.conf"]
-
启动
cd /usr/local/etc/redis/sentinel/server docker-compose up -d cd /usr/local/etc/redis/sentinel/sentinel docker-compose up -d #查看主从信息 info replication 测试 停止主节点 docker stop redis-server-master #进入从1redis-server-slave-1查看角色是否被修过为主节点 docker exec -it redis-server-slave-1 /bin/bash redis-cli -p 6380 -a 123456 info replication #进入从1redis-server-slave-2查看角色是否被修过为主节点 docker exec -it redis-server-slave-2 /bin/bash redis-cli -p 6381 -a 123456 info replication
注意:
1.哨兵是除了redis主从服务器外,还要至少三台(奇数)服务器做哨兵。
2.主节点宕机后,从节点在选举过程中无法写入数据,但是不会造成数据的丢失,但是在主节点向从节点更新数据的宕机会导致数据的丢失。
3.当主节点从新回来后,它会作为新的主节点下的从节点使用。
3. 数据问题
1.脑裂问题
出现了主节点和哨兵之间网络原因,而且有多数以上的哨兵认为主节点宕机,则再从会从节点现在一个主,这个时候客户端代码还是可以连接到之前的主节点的,可以写数据,此时哨兵选举了新的主节点,然后之前的主网络恢复了,然后之前的主节点备份现在的主节点数据,造成数据不完整。
解决办法:在配置文件中,配置从节点的个数至少大于本网络下的从节点数,例如:本网络下从节点是 2,则配置至少是3。意义就是主节点发现从节点个数小于3自己判断它与别的网络断开连接,然后停止对外的服务。配置文件为:“min-slaves-to-write” "0"
2.异步复制数据丢失问题
因为是异步复制数据,如果主节点和从节点直接数据复制太慢,在这之间主节点真实宕机,这个时候从节点替换主节点,丢失了数据。哨兵模式无法避免数据百分之白不丢失,只能通过配置尽可能减少数据的丢失。
解决办法:偏移量配置。主节点和从节点数据之前偏移量只差,如果偏移量只差比配置小,则主节点也不会提供服务。这个方法带来的问题,网络原因,导致复制慢,会误判为主节点宕机。所以,配置偏移量的大小需要在测试环境中测试,然后取合适的值。配置文件为:“min-slaves-max-lag” "10" 这里的配置主从的ack时间 ,也能代表偏移量。
10. 集群
主从结构,还有一个致命的缺点就是,不论从节点多少但是负责写的主节点只有一个。所以会影响系统的写的性能。随着程序的升级提供了集群的方式来解决主从结构的弊端。集群是由多个节点来提供服务的,它的实现有两种模式。
1. 副本集模式
多个节点都可以读和写,并且多个节点之间数据需要一致的。因为每个节点都需要负责所有的数据,所以会造成大量数据的冗余,而且为了达到所有节点负责所有数据的要求,必须解决数据一致性的问题,例如:不同的客户端连接不同的redis ,如果连接的其中一个redis 宕机,就无法保证这个redis的数据复制到其他的redis里面。
2. 分片模式
每个节点只负责一部分数据的写,由于数据做了分片的模式就会存在以下两个问题。
- 数据的均匀分配
- 节点的增加删除对数据分配的影响
3. 分片原理
redis集群数据分配规则:数据槽
哈希取余: 比如3个节点。
hash(key)%3= 0,1,2
1,2,3,4,5,6,7,8,9==3个节点
1,2,0,1,2,0,1,2,0
1,2,3,4,5,6,7,8,9==2个节点
1,2,0,1,2,0,1,2,0
1,0,1,0,1,0,1,0,1==90%的数据需要迁移-需要大量的时间计算分配那个节点,把数据传输到具体的节点。
一致性哈希:哈希环--通过哈希环的方式,来解决数据大量迁移的问题。先哈希,比如结果=1,则把数据存放在哈希环上面1-2直接的虚拟节点上面,都有依赖节点个数。
分槽:尽量让槽多一些,把数据分配到指定的16384个槽上面去,取余根据16383。这样就解决了数据分片不依赖节点的问题。
redis分片使用的就是 crc16算法%16384。在搭建集群的时候,就会把把16384个Has槽平均的分配到各个节点。
以下是结构图:
4. 数据保存流程
(1)客户端连接节点->(2)产生Key发送到对应的节点->(3)节点计算它的数据槽位置->(4)返回应该保存相应数据槽的节点的IP->(5)客户端重定向到新的节点IP保存数据。
5. 客户端连接集群
ServiceStack.Redis 是不支持Redis集群连接的,因为不支持重定向。
-
StackExchange.Redis 支持连接。
//连接的是主节点 IDatabase redis = ConnectionMultiplexer.Connect("127.0.0.1:7000,127.0.0.1:7001,127.0.0.1:7002,127.0.0.1:7003,127.0.0.1:7004,127.0.0.1:7005").GetDatabase(0); redis.StringSet("name", "sss");
在redis6.x 中有代理连接只需要连接一个地址。但是还是实验阶段。
6. 节点宕机处理
集群本身是具备集群和哨兵的功能的。
比如上图中的节点master 节点宕机,则该节点下的两个slave 会发起投票让节点B的mastar和节点C的mastar,投票的原则采用延迟公式。
投票延迟计算公式:DELAY = 500ms + random(0 ~ 500ms) + SLAVE_RANK * 1000ms。延迟时间越小,越能有大的几率成为Mastr。SLAVE_RANK表示此从节点已经从主节点复制数据的总量的等级,数据越新则等级越低,(实际测试衡量性能和数据偏移量的一个值)。
7. 集群的搭建
1. Windows 环境下搭建
# bind 127.0.0.1 //加上注释#
protected-mode no //关闭保护模式
port 6391 //绑定自定义端口
# daemonize yes //禁止redis后台运行
pidfile /var/run/redis_6391.pid
cluster-enabled yes //开启集群 把注释#去掉
cluster-config-file nodes_6391.conf //集群的配置 配置文件首次启动自动生成
appendonly yes //开启aof
cluster-announce-ip 10.xx.xx.xx //要宣布的IP地址。nat模式要指定宿主机IP
cluster-announce-port 6391 //要宣布的数据端口。
cluster-announce-bus-port 16391 //要宣布的集群总线端口
客户端连接集群命令:redis-cli -c -p 7000 //-c 表示集群连接
查看集群的信息命令:cluster nodes
以下是客户端连接集群的的状态:
2. Docker环境下搭建
mkdir /mydata/redis-cluster/config -p
cd /mydata/redis-cluster/config
赋值文件到此目录下
//docker-compose
version: "3"
services:
redis-master1:
image: redis # 基础镜像
container_name: node1 # 容器名称
working_dir: /config # 切换工作目录
environment: # 环境变量
- PORT=6391 # 会使用config/nodes-${PORT}.conf这个配置文件
ports: # 映射端口,对外提供服务
- 6391:6391 # redis的服务端口
- 16391:16391 # redis集群监控端口
stdin_open: true # 标准输入打开
tty: true # 后台运行不退出
network_mode: host # 使用host模式
privileged: true # 拥有容器内命令执行的权限
volumes:
- /mydata/redis-cluster/config:/config #配置文件目录映射到宿主机
entrypoint: # 设置服务默认的启动程序
- /bin/bash
- redis.sh
redis-master2:
image: redis
working_dir: /config
container_name: node2
environment:
- PORT=6392
ports:
- 6392:6392
- 16392:16392
stdin_open: true
network_mode: host
tty: true
privileged: true
volumes:
- /mydata/redis-cluster/config:/config
entrypoint:
- /bin/bash
- redis.sh
redis-master3:
image: redis
container_name: node3
working_dir: /config
environment:
- PORT=6393
ports:
- 6393:6393
- 16393:16393
stdin_open: true
network_mode: host
tty: true
privileged: true
volumes:
- /mydata/redis-cluster/config:/config
entrypoint:
- /bin/bash
- redis.sh
redis-slave1:
image: redis
container_name: node4
working_dir: /config
environment:
- PORT=6394
ports:
- 6394:6394
- 16394:16394
stdin_open: true
network_mode: host
tty: true
privileged: true
volumes:
- /mydata/redis-cluster/config:/config
entrypoint:
- /bin/bash
- redis.sh
redis-slave2:
image: redis
working_dir: /config
container_name: node5
environment:
- PORT=6395
ports:
- 6395:6395
- 16395:16395
stdin_open: true
network_mode: host
tty: true
privileged: true
volumes:
- /mydata/redis-cluster/config:/config
entrypoint:
- /bin/bash
- redis.sh
redis-slave3:
image: redis
container_name: node6
working_dir: /config
environment:
- PORT=6396
ports:
- 6396:6396
- 16396:16396
stdin_open: true
network_mode: host
tty: true
privileged: true
volumes:
- /mydata/redis-cluster/config:/config
entrypoint:
- /bin/bash
- redis.sh
redis-server /config/nodes-${PORT}.conf
cd /mydata/redis-cluster/config/
#启动节点
docker-compose up -d
#创建集群
#方案一
docker run --rm -it zvelo/redis-trib create --replicas 1 192.168.3.202:6391 192.168.3.202:6392 192.168.3.202:6393 192.168.3.202:6394 192.168.3.202:6395 192.168.3.202:6396
#方案二
#随便进入一个redis容器
docker exec -it 容器id /bin/bash
redis-cli --cluster create 192.168.3.202:6391 192.168.3.202:6392 192.168.3.202:6393 192.168.3.202:6394 192.168.3.202:6395 192.168.3.202:6396 --cluster-replicas 1
#验证测试
redis-cli -h 192.168.3.202 -p 6391 -c
#查看集群信息
cluster info
#查看节点信息
cluster nodes
docker下运行命令 :docker-compose up -d
客户端连接方式:redise-cli -h IP -p Port -c //-c 表示集群连接
8. 集群注意问题
-
集群要把数据都分配完成。集群状态发送了改变,然后数据要迁移。如果数据迁移时间长则整个服务无法使用,
表示:槽全部分配完可以使用,或者槽没有分配完也可以使用,这个就根据公司自己业务。
cluster-require-full*-coverage : no 没有完全分配就提供服务,yes,必须是全部搞完才分配
集群是做了分片的,如果事务或者lua涉及到key在多个节点里面都不支持,同样还有一些其他指令也是不支持。
集群里面数据库只要一个db,单机是有16个db。
不要使用单机,也不要使用哨兵,官方推荐使用集群,而且官方测试,集群节点最好不要大于1000个节点。
当redis 分片中master 与slave 都宕机则集群不可用,不对外提供服务。
redis集群最少是需要6个服务,三个master三个slave ,少于这些无法搭建。
一个客户端产生的key 在集群中由于分槽的原因可能会分到不同节点上。如果想控制一个客户端产生的key分到同一节点,则可以通过对key加{}的方式:系统功能,如果key里面有括弧, 表示哈希的时候就根据括弧里面的内容来哈希。
扩容:先启动节点,然后把节点加入到集群里面,再把其他节点的数据分配一些过来,可以选择分配个节点,以及数据量。
缩容:先把要删除的节点的数据迁移到其他节点,然后把这个节点在从集群移除。
集群优化,redis优化,如果是linux,根据不同版本,把同时能打开的文件句柄配置最大。
哨兵和redis 是平等关系。
集群本身具备负载均衡。
11.数据淘汰策略
使用maxmemory配置是为了将Redis配置为对数据集使用指定的内存量,
-
直接修改redis.conf文件
maxmemory 100mb (maxmemory 0mb :设置maxmemory为零将导致没有内存限制)
-
直接连接redis执行
config set maxmemory 100mb
-
查看内存的使用
config get maxmemory
当内存使用值超过了maxmemory配置时,redis可以使用以下策略进行数据淘汰:
noeviction:(默认策略)当达到内存限制并且客户端尝试执行可能导致使用更多内
存的命令时返回错。
//以下可能把使用频率低的删除,有可能删除我们使用周期长但是频率低的数据
volatile-lru:删除设置了过期时间的key而且最近最少使用key(LRU算法淘汰)。
allkeys-lru:删除所有比较最近最少使用的key(LRU算法淘汰)。
//官方推荐下面两种,删除有用数据的概率比较低
volatile-lfu:删除设置了过期时间的key而且使用频率最低key(LFU算法淘汰)。
allkeys-lfu:删除所有使用频率最低的key(LFU算法淘汰)。
volatile-random:设置了过期时间的key使用随机淘汰。
allkeys-random:所有key使用随机淘汰。
volatile-ttl:设置了过期时间的key根据过期时间淘汰,越早过期越早淘汰。
12.一些常遇到的问题
1.缓存击穿
问题:表示Key不在Redis中,在数据库中,原因这个Key过期。
解答:
不要设置这个Key的过期时间。
2.缓存穿透
问题:数据不在关系型数据库,也不在redis 中,业务逻辑为:先查询Redis ,如果查询不到则在关系型数据库中查询。这样导致的问题就是:1.造成关系型数据库的压力,2.数据本身不存在。(恶意请求)
解答:有两种解决办法
- 第一次,查询数据Redis与关系型数据库都没有,则把这个Key 保存到Redis中值为:null,以后查询这个Key返回null即可。(不推介:可能会占用大量的内存)
- 使用布隆过滤器:每次操作Key,把Key写入布隆过滤器中。新来的先查询布隆过滤器,如果有继续查询Redis 和关系型数据库,如果都没有直接返回。布隆过滤器的误判可以忽略不记。
3.缓存雪崩
问题:同一时间有大量的Key过期,导致数据请求失败,程序转到查询关系型数据库,并发量过大导致服务不可用。
解答:不要设置同样的过期时间,提供一个过期因子,1-1000 秒的随机数,加入到过期时间中。
4.缓存预热
问题:启动项目的时候,先查询Redis ,如果Redis没有查询数据库,然后把查询到的数据放入Redis中,做下次的查询。这个过程导致启动慢和数据库压力。
解答:
启动之前提供其他的程序把数据从数据库读入到Redis中。对于过期的数据,再提供一个程序定期的把数据写入到Redis。
5.碎片管理
问题:Redis使用太久,内存占用过大,但是删除了一些Key 内存还是没有降下来。
解答:有两种方式
- 重启Redis。
- 修改配置文件,表示内存使用一定百分比自动去做碎片整理。注意:碎片整理太过平凡会导致Redis性能下降