1.课外知识:
一扇区 : 512Byte 操作系统最小分配磁盘大小 : 4K(可自定义,取决于上层应用,IO密集型可加大)
关系型数据的库表很大,会导致性能下降?
1.增删改会变慢(需要维护索引)
2.查询速度:
2.1 :一个或少量查询依然很快(走索引)
2.2 :并发量大的时候会受到硬盘带宽的影响
权威查看数据库排名 :http://db-engines.com
redis.windows.conf 为redis相关配置
redis强调最终一致性,而不是实时一致性
启动redis
1.redis目录下 /src ./redis-server
2.配置了redis服务的 : service redis_6379 start
3.指定配置文件启动rdis : redis-server ./redis.conf
4.启动redis并追随一个主节点 : redis-server ./redis.conf --replicaof 127.0.0.1 6379
关闭redis服务
1.直接杀死进程: ps -ef | grep redis 再 kill -9 进程号
2.配置了redis服务的: rm -rf /var/run/redis_6379.pid
提出问题: redis为啥这么快?
2.内核发展:
2.1 BIO fd(文件描述符)必须等fd8读完, 才会有fd9去读.
BIO是一个连接一个线程.
BIO的大并发问题:在使用同步I/O的网络应用中,如果要同时处理多个客户端请求,或是在客户端要同时和多个服务器进行通讯,就必须使用多线程来处理。也就是说,将每一个客户端请求分配给一个线程来单独处理。这样做虽然可以达到我们的要求,但同时又会带来另外一个问题。由于每创建一个线程,就要为这个线程分配一定的内存空间(也叫工作存储器),而且操作系统本身也对线程的总数有一定的限制。
2.2 NIO while(){fd8,fd9…} 能读就读,轮询
NIO是一个请求一个线程(非有效请求即不间断查询是否读完)
NIO的最重要的地方是当一个连接创建后,不需要对应一个线程,这个连接会被注册到多路复用器上面,所以所有的连接只需要一个线程就可以搞定,当这个线程中的多路复用器进行轮询的时候,发现连接上有请求的话,才开启一个线程进行处理,也就是一个请求一个线程模式。
对于2.1的优化: 提高cpu的利用率(对于BIO等待fd8 时会阻塞)
2.3 select函数 ,只调用一次内核,内核选择一些要IO的,并将FD返回,进程/线程拿到返回的的文件描述符,减少用户态的切换
对于2.2的优化 :减少调用Kernel的次数
select函数原型: int select(int n,fd_set * readfds,fd_set * writefds,fd_set * exceptfds,struct timeval * timeout);
select的调用过程:陷入内核,构造辅助数据结构poll_wqueues,第一次执行poll_wait,为每一个待监视的设备描述符fd构造一个poll_table_entry加入到poll_w_queues中,并将当前进程加入到驱动程序的相应的等待队列中,同时记录下每个设备状态,循环完所有的fd之后,如果有设备就绪,则可以立刻返回,否则进入睡眠。被唤醒时,还会再循环一次所有设备的驱动中的poll函数,不管是设备准备就绪唤醒的还是超时时间到唤醒的都会执行这个操作,只是这次不会再有将进程加入队列的操作。这次同样记录下每一个设备的状态,并拷贝到用户空间中,然后将当前进程从队列中脱链,然后返回到用户空间。
4.kernel 的基于 Epoll 的同步,非阻塞的多路复用
对于3的优化 :减少select与进程/线程的拷贝,增加共享空间,进程/线程放,seelct取
- IO多路复用是一种同步IO模型,实现一个线程可以监视多个文件句柄;一旦某个文件句柄就绪,就能够通知应用程序进行相应的读写操作;没有文件句柄就绪时会阻塞应用程序,交出cpu。多路是指网络连接,复用指的是同一个线程
3.redis数据类型:
redis类型并不是很重要,重要的是redis对每种类型的数据都有自己的操作方法,无需在客户端对取出的元素进行转换 (计算向数据移动)
3.1 String
//1.添加/修改数据
set key value
//2.获取数据
get key
//3.删除数据
del key
/4./添加/修改多个数据
mset key1 valueq key2 value2 …
//5.获取多个数据
mget key1 key2 …
//6.获取数据字符个数(字符串长度)
strlen key
//7.追加信息到原始信息后部(如果原始信息存在就追加,否则新建)
append key value
String作为数值操作时的注意事项
1.string在redis内部存储默认就是一个字符串,当遇到增减类操作incr,decr时会转成数值型进行计算
2.redis所有的操作都是原子性的,采用单线程处理所有业务,命令是一个一个执行的,因此无需考虑并发带来的数据影响。
3.按数值进行操作的数据,如果原始数据不能转成数值,或超过了redis数值上线范围,将会报错。9223372036854775807 (java中long型数据最大值,Long.MAX_VALUE)
业务场景:
场景一:“某某综艺”,启动海选投票,只能通过微信投票,每个微信号每4个小时只能投1票。
场景二:电商商家开启热门商品推荐,热门商品不能一直处于热门期,每种商品热门期维持3天,3天后自动取消热门
场景三:新闻网站会出现热点新闻,热点新闻最大的特征是对时效性,如何自动控制热点新闻的时效性
通用操作:
- set k v nx/xx (nx:不存在时更改 xx:存在时进行修改)
- object encoding key (查看键的类型)
3.1.2 位图(bitmap)
帮助文档:help @bitmap
redis是二进制安全的: 采用字节流 redis-cli --raw (格式化)底层存的字节
位图( bitmap )
# 将mybitmap偏移量为0的位置设置值为1
SETBIT mybitmap 0 1
# 将mybitmap偏移量为1的位置设置值为0
SETBIT mybitmap 1 0
# 将mybitmap偏移量为2的位置设置值为1
SETBIT mybitmap 2 1
# 获取mybitmap偏移量为1上的值
GETBIT mybitmap 1
# 获取mybitmap中值为1的二进制位数量,此处为2
BITCOUNT mybitmap
#获取mybitmap的第一个字节和第二个字节中有多少个二进制位被设置成了1
BITCOUNT mybitmap 0 1
# 获取mybitmap中第一个二进制位值为1的偏移量
BITOPS mybitmap 1
BITSET mybitmap2 0 1
# 对mybitmap和mybitmap2执行或操作,并将结果存储到result_map or/and/xor/no
BITOP OR result_bitmap mybitmap mybitmap2
应用场景:
场景1: 统计用户365天登录的天数(如 01010000 表示第2 4天都登录了)用户为key ,天为键
所需内存大小 : 46B * 用户数(10000000) =460 000 000 一千万用户只需460MB
场景2: 统计活跃用户 : 天为键, 值为用户
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FSC5EWsQ-1650165247295)(C:\Users\HP\Desktop\学习方向\Redis\图片\统计活跃用户.PNG)]
键为 日期字符串,二进制0101,一个二进制位对应一个确定的用户,第几位指向第几个用户(20190101 01000 表示第二位为1对应的用户2在20190101登陆过) 再选择一个日期区间做 或 运算,将结果取 bitcount 统计1的个数就是那几天的活跃用户
所需内存:10 000 000 用户 = 1MB * 天数
3.2 List:
help @list
存储多个数据,并对数据进入存储空间的顺序进行区分 list可以用来:
排队消费 :blpop key key对应的list没有值将会阻塞, 存在值才会执行
list的底层数据结构 : 双向链表 头尾指针分别指向链表的头和尾,方便遍历
list简单操作:
// 1. 添加/修改数据
lpush/rpush key value1 value2 …
// 2. 获取数据
lrange key start stop //获取从左数第start到stop个元素,从0开始 -1结束
lindex key index //查询第i个元素
// 3. list长度
llen key //list的长度
// 4. 获取并移除数据
lpop key //获取并删除左边第一个元素
rpop key //获取并删除右边第一个元素
场景: 消息队列
3.3hash:
help @hash
为了区别与Redis中的键值对的称呼,hash中的键成为field,而key特征Redis的键。
借用一下别人的图:
hash 操作
// 1. 添加,修改 / 获取
hset key field value / hget key field
// 2. 获取多个field
hmset/hmget key field1 field2 field3....
// 3. 删除一个或多个field
hdel key field1 field2....
// 4. 获取哈希表中field的数量
hlen key
// 5. 获取哈希表中是否存在指定的field
hexists key field
// 6. 获取哈希表中所有的 field 和 value
hkeys key //字段名
hvals key //字段值
// 7. 设置指定字段的数值数据增加指定范围的值
hincrby key field increment //指定数值增长increment的值(整数)
hincrbyfloat key field increment //指定数值增长increment的值(浮点数)
// 8. 获取hash所有field和value
hgetall //hgetall操作可以获取全部属性,如果内部fiekd过多,遍历整体数据效率就会很低,有可能成为数据访问瓶颈。
实战场景:
1.缓存: 能直观,相比string更节省空间,的维护缓存信息,如用户信息,视频信息等
3.4 set: (不维护排序,存放顺序)
help @set
优点: 存储大量的数据,在查询方面提供更高的效率
set操作:
// 1. 获取全部数据
smembers key
// 2. 获取集合数据总量
scard key
// 3. 添加一条或多条数据
sadd key menber1 member2...
// 4. 删除一条或多条数据
srem key member1 member2...
// 5. 交
sinter k1 k2 或 sinterstore dest 2 k1 k2... 放到dest目标set
// 6. 并
sunion k1 k2 或 sunionstore dest 2 k1 k2... 放到dest目标set
// 7. 差
diff k1 k2 或 sdiffstore dest 2 k1 k2... 放到dest目标set
// 8. 随机事件
srandmember key count
count > 0 :取出去重的结果集(不能超过已有集)
count < 0 : 取出一个有重复的结果集,一定满足你要的数量
count=0 不返回
应用场景 : 抽奖
3.5 zset (sorted set)
help @zset
根据排序有利于数据的有效显示,需要提供一种可以根据自身特征(scope)进行排序的方式
sorted set :物理内存scope左小右大
存储结构: skip list 跳跃表
zset操作:
// 1. 添加数据
zadd key score1 member1 score2 member2 ...
// 2. 删除数据
zrem key member1 member2
// 3. 获取全部set数据
zrange key start stop WITHSCORES //小到大(按scope分值)
zrevrange key start stop WITHSCORES //大到小
// 4. 获取数据
zrangebyscore key min max [WITHSCORES] [LIMIT] //查询scores在某个范围内的值,WITHSCORES 要不要显示分数 limit限制查几个
zrevrangebyscore key max min [WITHSCORES] //查询key某个索引范围内的值
// 5. 删除
zremrangebyrank key start stop //索引范围
zremrangebyscore key min max //scope分值范围
// 6. 一共多少条数据
zcard key //获取总量
// 7. 交 dest为目标集合, 3为一共多少个集合求交集
zinterstore dest 3 k1,k2,k3
zunionstore dest 3 k1,k2,k3
zdiffstore dest 3 k1,k2,k3
//8. 查key对应的scope
zscope k1 apple
//9.查key的排名
zrank k1 apple
应用场景: 排行榜
4.Jedis API:
@Test
public void keyTest() throws UnsupportedEncodingException {
System.out.println(jedis.flushDB());// 清空数据
System.out.println(jedis.echo("hello"));
// 判断key否存在
System.out.println(jedis.exists("foo"));
jedis.set("key", "values");
jedis.set("key2", "values");
System.out.println(jedis.exists("key"));// 判断是否存在
// 如果数据库没有任何key,返回nil,否则返回数据库中一个随机的key。
String randomKey = jedis.randomKey();
System.out.println("randomKey: " + randomKey);
// 设置60秒后该key过期
jedis.expire("key", 60);
// key有效毫秒数
System.out.println(jedis.pttl("key"));
// 移除key的过期时间
jedis.persist("key");
// 获取key的类型, "string", "list", "set". "none" none表示key不存在
System.out.println("type: " + jedis.type("key"));
// 导出key的值
byte[] bytes = jedis.dump("key");
System.out.println(new String(bytes));
// 将key重命名
jedis.renamenx("key", "keytest");
System.out.println("key是否存在: " + jedis.exists("key"));// 判断是否存在
System.out.println("keytest是否存在: " + jedis.exists("keytest"));// 判断是否存在
// 查询匹配的key
// KEYS * 匹配数据库中所有 key 。
// KEYS h?llo 匹配 hello , hallo 和 hxllo 等。
// KEYS h*llo 匹配 hllo 和 heeeeello 等。
// KEYS h[ae]llo 匹配 hello 和 hallo ,但不匹配 hillo 。
// 特殊符号用 \ 隔开。
Set<String> set = jedis.keys("k*");
System.out.println(set);
// 删除key
jedis.del("key");
System.out.println(jedis.exists("key"));
}
4.1 String类型
1.选择数据库 : select 1
2.设值: set name zhangsan
3.取值: get name
4.批量设值: mset sex 1 addr sh
5.批量取值: mget set addr
@Test
public void stringTest() {
jedis.set("hello", "hello");
System.out.println(jedis.get("hello"));
// 使用append 向字符串后面添加
jedis.append("hello", " world");
System.out.println(jedis.get("hello"));
// set覆盖字符串
jedis.set("hello", "123");
System.out.println(jedis.get("hello"));
// 设置过期时间
jedis.setex("hello2", 2, "world2");
System.out.println(jedis.get("hello2"));
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
}
System.out.println(jedis.get("hello2"));
// 一次添加多个key-value对
jedis.mset("a", "1", "b", "2");
// 获取a和b的value
List<String> valus = jedis.mget("a", "b");
System.out.println(valus);
// 批量删除
jedis.del("a", "b");
System.out.println(jedis.exists("a"));
System.out.println(jedis.exists("b"));
}
4.2hash 类型
1.设值: hset user name zhangsan user为redis的key name为hash的key zhangsan为hash的value
2.取值: hget user name
3.批量设值: hmset user age 18 sex 1
4.批量取值: hmget user age sex
5.取redis的key: hgetall user
6.删除 : hdel user name age //删除两个name和age
例子
@Test
public void testHash() {
// 清空数据
System.out.println(jedis.flushDB());
String key = "myhash";
Map<String, String> hash = new HashMap<String, String>();
hash.put("aaa", "11");
hash.put("bbb", "22");
hash.put("ccc", "33");
// 添加数据
jedis.hmset(key, hash);
jedis.hset(key, "ddd", "44");
// 获取hash的所有元素(key值)
System.out.println(jedis.hkeys(key));
// 获取hash中所有的key对应的value值
System.out.println(jedis.hvals(key));
// 获取hash里所有元素的数量
System.out.println(jedis.hlen(key));
// 获取hash中全部的域和值,以Map<String, String> 的形式返回
Map<String, String> elements = jedis.hgetAll(key);
System.out.println(elements);
// 判断给定key值是否存在于哈希集中
System.out.println(jedis.hexists(key, "bbb"));
// 获取hash里面指定字段对应的值
System.out.println(jedis.hmget(key, "aaa", "bbb"));
// 获取指定的值
System.out.println(jedis.hget(key, "aaa"));
// 删除指定的值
System.out.println(jedis.hdel(key, "aaa"));
System.out.println(jedis.hgetAll(key));
// 为key中的域 field 的值加上增量 increment
System.out.println(jedis.hincrBy(key, "bbb", 100));
System.out.println(jedis.hgetAll(key));
}
4.3 list类型(可重复值)
1.左添加: lpush student zhangsan lisi //添加类似于队列,会把前面的数据推到后面
2.右添加: rpush student wangwu zhaoliu // 添加之后为 l z w zhaoliu
3.查看范围的数据 : lrange student 0 3
4.长度: llen student
5.删除: lrem student 1 lisi //删除一个李四(1对应删除从左往右1个lisi)
@Test
public void listTest() {
String key = "mylist";
jedis.del(key);
// 队列添加元素
jedis.rpush(key, "aaaa");
jedis.rpush(key, "aaaa");
jedis.rpush(key, "bbbb");
jedis.rpush(key, "cccc");
jedis.rpush(key, "cccc");
// 队列长度
System.out.println("lenth: " + jedis.llen(key));
// 打印队列,从索引0开始,到倒数第1个(全部元素)
System.out.println("all elements: " + jedis.lrange(key, 0, -1));
// 索引为1的元素
System.out.println("index of 1: " + jedis.lindex(key, 1));
// 设置队列里面一个元素的值,当index超出范围时会返回一个error。
jedis.lset(key, 1, "aa22");
System.out.println("index of 1: " + jedis.lindex(key, 1));
// 从队列的右边入队一个元素
jedis.rpush(key, "-2", "-1");// 先-2,后-1入队列
System.out.println("all elements: " + jedis.lrange(key, 0, -1));
// 从队列的左边入队一个或多个元素
jedis.lpush(key, "second element", "first element");// 先second
// element,后first
// elementF入队列
System.out.println("all elements: " + jedis.lrange(key, 0, -1));
// 从队列的右边出队一个元素
System.out.println(jedis.rpop(key));
// 从队列的左边出队一个元素
System.out.println(jedis.lpop(key));
System.out.println("all elements: " + jedis.lrange(key, 0, -1));
// count > 0: 从头往尾移除值为 value 的元素,count为移除的个数。
// count < 0: 从尾往头移除值为 value 的元素,count为移除的个数。
// count = 0: 移除所有值为 value 的元素。
jedis.lrem(key, 1, "cccc");
System.out.println("all elements: " + jedis.lrange(key, 0, -1));
// 即最右边的那个元素也会被包含在内。 如果start比list的尾部下标大的时候,会返回一个空列表。
// 如果stop比list的实际尾部大的时候,Redis会当它是最后一个元素的下标。
System.out.println(jedis.lrange(key, 0, 2));
System.out.println("all elements: " + jedis.lrange(key, 0, -1));
// 删除区间以外的元素
System.out.println(jedis.ltrim(key, 0, 2));
System.out.println("all elements: " + jedis.lrange(key, 0, -1));
}
4.4 set类型(无序集合(内部排序是固定的))
1.添加 : sadd letters a b c d e
2.获取全部数据: smembers letters
3.查数据条数: scard letters
4.删除数据: srem letters a c
@Test
public void testSet() {
// 清空数据
System.out.println(jedis.flushDB());
String key = "myset";
String key2 = "myset2";
// 集合添加元素
jedis.sadd(key, "aaa", "bbb", "ccc");
jedis.sadd(key2, "bbb", "ccc", "ddd");
// 获取集合里面的元素数量
System.out.println(jedis.scard(key));
// 获得两个集合的交集,并存储在一个关键的结果集
jedis.sinterstore("destination", key, key2);
System.out.println(jedis.smembers("destination"));
// 获得两个集合的并集,并存储在一个关键的结果集
jedis.sunionstore("destination", key, key2);
System.out.println(jedis.smembers("destination"));
// key集合中,key2集合没有的元素,并存储在一个关键的结果集
jedis.sdiffstore("destination", key, key2);
System.out.println(jedis.smembers("destination"));
// 确定某个元素是一个集合的成员
System.out.println(jedis.sismember(key, "aaa"));
// 从key集合里面随机获取一个元素
System.out.println(jedis.srandmember(key));
// aaa从key移动到key2集合
jedis.smove(key, key2, "aaa");
System.out.println(jedis.smembers(key));
System.out.println(jedis.smembers(key2));
// 删除并获取一个集合里面的元素
System.out.println(jedis.spop(key));
// 从集合里删除一个或多个元素
jedis.srem(key2, "ccc", "ddd");
System.out.println(jedis.smembers(key2));
}
4.5 sorted set 类型
1.添加:zadd score 7 zhangsan 3 lisi 6 wangwu
2.查看数据; zrange score 0 4 //按分数从小到大排序
3.查看条数; zcard score
4.删除: zrem score 0 1 //删除第0 1条数据(zhangsan lisi)
@Test
public void testSortSet() {
// 清空数据
System.out.println(jedis.flushDB());
String key = "mysortset";
Map<String, Double> scoreMembers = new HashMap<String, Double>();
scoreMembers.put("aaa", 1001.0);
scoreMembers.put("bbb", 1002.0);
scoreMembers.put("ccc", 1003.0);
// 添加数据
jedis.zadd(key, 1004.0, "ddd");
jedis.zadd(key, scoreMembers);
// 获取一个排序的集合中的成员数量
System.out.println(jedis.zcard(key));
// 返回的成员在指定范围内的有序集合,以0表示有序集第一个成员,以1表示有序集第二个成员,以此类推。
// 负数下标,以-1表示最后一个成员,-2表示倒数第二个成员
Set<String> coll = jedis.zrange(key, 0, -1);
System.out.println(coll);
// 返回的成员在指定范围内的逆序集合
coll = jedis.zrevrange(key, 0, -1);
System.out.println(coll);
// 元素下标
System.out.println(jedis.zscore(key, "bbb"));
// 删除元素
System.out.println(jedis.zrem(key, "aaa"));
System.out.println(jedis.zrange(key, 0, -1));
// 给定值范围内的成员数
System.out.println(jedis.zcount(key, 1002.0, 1003.0));
}
4.6 jedis操作事务(只会回滚当前数据库出错的)
@Test
public void testTransaction() {
Transaction t = jedis.multi();
t.set("hello", "world");
Response<String> response = t.get("hello");
t.zadd("foo", 1, "barowitch");
t.zadd("foo", 0, "barinsky");
t.zadd("foo", 0, "barikoviev");
Response<Set<String>> sose = t.zrange("foo", 0, -1); // 返回全部相应并以有序集合的方式返回
System.out.println(response);
System.out.println(sose);
t.exec(); // 此行注意,不能缺少
String foolbar = response.get(); // Response.get() 可以从响应中获取数据
int soseSize = sose.get().size(); // sose.get() 会立即调用set方法
System.out.println(foolbar);
System.out.println(sose.get());
}
mutli / exec (多个客户端连接服务端并开启事务,服务端必须等到事务的exec命令才会将一个事务的全部命令一起执行(没等到exec命令之前将命令存起来并不执行))
watch :乐观锁CAS(检查版本)版本不对不会执行 返回nil
为什么redis不支持回滚:
1.redis命令只会因为错误的语法而失败,编程错误
2.保持redis内部快捷
且发生错误事务不会执行
4.7 Jedis操作管道
将本来多条redis命令压缩成一条命令发送给redis服务端,减少网络IO
@Test
public void testTransactionPipeling() {
Pipeline p = jedis.pipelined();//开一个管道
p.set("fool", "bar");
p.zadd("foo", 1, "barowitch");
p.zadd("foo", 0, "barinsky");
p.zadd("foo", 0, "barikoviev");
Response<String> pipeString = p.get("fool");
Response<Set<String>> sose = p.zrange("foo", 0, -1);
System.out.println(pipeString);
System.out.println(sose);
p.sync();//提交
System.out.println("==========");
System.out.println(p.get("fool"));
System.out.println(p.zrange("foo", 0, -1));
int soseSize = sose.get().size();
Set<String> setBack = sose.get();
System.out.println(soseSize);
System.out.println(setBack);
}
Redis 管道: 将多个请求压缩成一个请求,减少网络IO次数
linux 管道:
1.前一个命令的输出作为后一个命令的输入
2.管道会触发子进程
例:
echo $$ | more
echo $BASHPID | more
$$(取进程号) 高于 |
父子进程 (pstree(进程树)): 子进程的修改无法破坏父进程的数据,父进程的修改也无法破坏子进程的数据(常规思想)
原理:fork(系统调用,快速创建子进程) + COW (copy on write 写时复制,内核机制) 创建子进程会获取到redis父进程的全部指针,在创建子进程之后的父进程对数据的修改不会改变子进程的数据。
4.8通用命令
1.层级关系目录: set cart:user01:item01 apple
2.通用删除: del 键
3.设置失效时间(t=-1:永不失效 t=-2:已经过期):
ex <ttl> :将key的生存时间设置为t秒
px <ttl> :将key的生存时间设置为t毫秒
例:
set code test ex 10 //设置value=test的失效时间为10s
set code2 test //永不失效, ttl=-1
ttl code //查看key=code还剩时间,ttl=-2 表示失效
expire code2 10 //给已存在的键code2设置失效时间
set code3 test px 10000 xx //只给已存在的键设失效时间,不存在的会返回nil
set code3 test px 10000 nx //只给不存在的键设失效时间,不存在的会返回nil
jedis线程不安全 redis1.0 以上采用 jedis连接池,2.0以上采用lettuce连接池
4.9 redis操作byte数组
/**
* 将对象-->byte[] (由于jedis中不支持直接存储object所以转换成byte[]存入)
* @param object
* @return
*/
private static byte[] serialize(Object object) {
ObjectOutputStream oos = null;
ByteArrayOutputStream baos = null;
try {
// 序列化
baos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(baos);
oos.writeObject(object);
byte[] bytes = baos.toByteArray();
return bytes;
} catch (Exception e) {
logger.error("{}", e);
} finally {
try {
oos.close();
baos.close();
} catch (IOException e) {
logger.error("{}", e);
}
}
return null;
}
/**
* 将byte[] -->Object
* @param bytes
*/
private static Object unserialize(byte[] bytes) {
ByteArrayInputStream bais = null;
try {
// 反序列化
bais = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bais);
return ois.readObject();
} catch (Exception e) {
logger.error("{}", e);
} finally {
try {
bais.close();
} catch (IOException e) {
logger.error("{}", e);
}
}
return null;
}
5.redis进阶操作:
5.1消息订阅
发布订阅(实时性) :
publish channel message (如 publish ch hello)
subscribe channel 监听这个信道 (如 subscribe ch)
5.2 布隆过滤器:
1.使用redis安装目录下的 utils 下的 install_service 程序创建一个服务 redis_6379
2.redis 启动服务 : service redis_6379 start (配置文件在/etc/redis/6379.conf)
3.redis连接服务端 redis-cli -p 6379
布隆过滤器工作原理:
先将数据库有的元素经过映射函数映射到bitmap里,将对应位置数置为1.。
将用户搜寻的关键字经过映射函数,去找bitmap里对应有没有1。
也有少概率事件:元素3(用户请求)匹配上了多个元素对应的1(误打误撞,碰巧),也会放过去(只要有一个位置出现0就过滤掉),但已经阻止90%的非法请求。
发生缓存击穿时: client客户端增加一个key,value的标记(错误),下一次就不会再访问数据库了。防止缓存击穿
若数据库增加了元素,需要完成元素的对bloom的增加
集成布隆过滤器的方案:
- client客户端实现bloom算法和bitmap,redis只做缓存
- client客户端实现bloom算法,redis维护bitmap
- client客户端啥也不干,redis 实现bloom算法和维护bitmap (符合微服务的设计,轻量客户端)
5.3 Redis如何淘汰过期的keys
Redis keys过期有两种方式:被动和主动方式。
例:具体就是Redis每秒10次做的事情:
- 测试随机的20个keys进行相关过期检测。
- 删除所有已经过期的keys。
- 如果有多于25%的keys过期,重复步奏
这是一个平凡的概率算法,基本上的假设是,我们的样本是这个密钥控件,并且我们不断重复过期检测,直到过期的keys的百分百低于25%,这意味着,在任何给定的时刻,最多会清除1/4的过期keys。
具体淘汰策略:
第一种:定时检查删除
对于每一个设置了过期时间的 key 都会创建一个定时器,一旦达到过期时间都会删除。这种方式立即清除过期数据,对内存比较好,
但是有缺点是:占用了大量 CPU 的资源去处理过期数据,会影响 redis 的吞吐量 和 响应时间。
第二种:惰性检查删除
当访问一个 key 的时候,才会判断该 key 是否过期,如果过期就删除。该方式能最大限度节省 CPU 的资源。
但是对内存不太好,有一种比较极端的情况:出现大量的过期 key 没有被再次访问,因为不会被清除,导致占用了大量的内存。
第三种:定期检查删除
每隔一段时间,扫描redis 中过期key 的字典,并清除部分过期的key。这种方式是前俩种一种折中方法。不同的情况下,调整定时扫描时间间隔,让CPU 与 内存达到最优。
内存淘汰策略
redis 内存淘汰策略是指达到maxmemory极限时,使用某种算法来决定来清理哪些数据,以保证新数据存入。
第一类 不处理,等报错(默认的配置)
noeviction,发现内存不够时,不删除key,执行写入命令时直接返回错误信息。(Redis默认的配置就是noeviction)
第二类 从所有结果集中的key中挑选,进行淘汰
1.allkeys-random 就是从所有的key中随机挑选key,进行淘汰
2.allkeys-lru 就是从所有的key中挑选最近使用时间距离现在最远的key,进行淘汰
3.allkeys-lfu 就是从所有的key中挑选使用频率最低的key,进行淘汰。(这是Redis 4.0版本后新增的策略)
第三类 从设置了过期时间的key中挑选,进行淘汰
这种就是从设置了expires过期时间的结果集中选出一部分key淘汰,挑选的算法有:
1.volatile-random 从设置了过期时间的结果集中随机挑选key删除。
2.volatile-lru 从设置了过期时间的结果集中挑选上次使用时间距离现在最久的key开始删除
3.volatile-ttl 从设置了过期时间的结果集中挑选可存活时间最短的key开始删除(也就是从哪些快要过期的key中先删除)
4.volatile-lfu 从过期时间的结果集中选择使用频率最低的key开始删除(这是Redis 4.0版本后新增的策略)
5.4 redis磁盘持久化方案:
1.bgsave(background save 开启后台保存命令,持久化到磁盘)
命令 :save 手动数据存储
优点: 1.简单,
缺点: 1.需要频繁写命令
2.RDB 内存快照(允许少部分数据丢失,数据持久化时阻塞,全量数据持久化)
原理: folk(系统调用,保证快速创建一个子进程)+ COW(内核操作,写时复制,并不是发生正真的复制操作,主进程创建一个子进程,子进程指针拥有的数据只是创建子进程那一刻父进程所拥有的全部数据(即子进程拥有父进程指向数据的指针),之后父进程对数据的操作并不改变子进程所有数据的指向)
本质:Redis按照一定的时间周期将目前服务中的所有数据全部写入到磁盘中
conf文件中的配置:
save 900 1
save 300 10
save 60 10000
例:
save 900 1 (每900s有一个key发生变化,就会存在dump.rdb上,下次启动redis时就会先去读取dump.rdb文件就好像存入磁盘
优点:
1.快照类似于java种的序列化,可以快速恢复
缺点:
1.还是有可能丢失数据(时点与时点之间窗口数据容易丢失),持久化时阻塞
2.不支持拉链,有一个dump.rdb文件,比如每晚都删除之前的创建新的,需要手动记录每次的dump.rdb文件
- AOF (append only file)
如果同时开启了AOF 和RDB,就只会用AOF恢复
conf文件中的配置
appendonly no (yes为开启aof,rdb配置会自动失效)
本质:忠实记录Redis服务启动成功后的每一次影响数据状态的操作命令,以便在Redis服务异常崩溃的情况出现时,可以按照这些操作命令恢复数据状态。既然要记录每次影响数据状态的操作命令,就意味着AOF文件会越来越大!
优点:
数据丢失较小
缺点:
时间积累,aof文件会非常大,恢复慢
开启AOF :在 redis配置文件里 appendonly yes
开启AOF RDB 混合 : aof-use-rdb-preamble yes
重写 : redis 4.0之前 用命令bgrewriteaof 重写aof文件,将重复无用的操作删除
redis4.0之后 :rdb+aof
自动化重写 :auto-aof-rewrite-min-size 64MB (达到64MB就重写) 且 auto-aof-rewrite-percentage 100 redis会记录最后一次rewrite时aof文件的大小
6.redis集群
CAP原则: 一致性(Consistency),可用性(Availability),分区容忍性(Partition tolerance) 3者只能同时实现两点,不可能三者兼顾。
单机,单节点,单实例缺点:
1.单点故障
2.容量有限
3.IO压力
AKF 微服务拆分
1.X轴 :全量,镜像的
2.y轴 :业务,功能的
3.Z轴 :优先级,逻辑再拆分
1对多出现的问题:
数据一致性(强一致性:所有节点同步阻塞,直到数据全部一致),但会破坏可用性。
主从:都可以访问主,从己节点。(需要对主节点做高可用(主从复制))
从机:奇数态,从机票数过半,认定主机挂了(从机3台比4台的优势: 4台从机有发生故障的概率更大,且需要3台投票一致才能认为主机挂了,相比于3台从机只需要2台就能投票判定。2太从机具有较小的成本,且发生宕机的概率更小)
主备:客户端只访问主节点,只有当主节点挂掉后,备用启用。
主从搭建:
启动客户端 : redis-cli -p 6380 redis-cli -p 6381
从机客户端命令: help @replicaof (帮助命令)
replicaof 主机 端口 如(replicaof 127.0.0.1 6379)
从机追随主机会 先 flush DB 再同步数据过来(从机默认情况下只读)
从机挂掉后: 从机重新连接主机,会同步RDB文件,以达到数据的同步,RDB会记录之前从机追随的主机的数据(追随主机的数据),新增的aof增量数据文件则不会有主机的信息。
从机退出追随 : slaveof no one
哨兵模式:本质(redis的发布订阅机制 publish)
主服务器挂机之后,哨兵会重新选取从服务器中的某一个变成新的主服务器提供独写能力。
1.启动
redis-server ./6379.conf
redis-server ./6380.conf --replicaof 127.0.0.1 6379
redis-server ./6381.conf --replicaof 127.0.0.1 6379
先创建配置文件 : 26379.conf ,26380.conf ,26381.conf
port 26379
sentinel monitor mymaster 127.0.0.1 6379 2 //mymaster:监控逻辑名称 2:投票权重
port 26380
sentinel monitor mymaster 127.0.0.1 6379 2 //mymaster:监控逻辑名称 2:投票权重
port 26381
sentinel monitor mymaster 127.0.0.1 6379 2 //mymaster:监控逻辑名称 2:投票权重
配置哨兵:
redis-server ./26379.conf --sentinel
redis-server ./26380.conf --sentinel
redis-server ./26381.conf --sentinel
所有的哨兵只知道master节点的信息,master节点通过发布订阅方式发现其他哨兵,其他哨兵才能获取到其他哨兵的信息。
当主节点宕机时 ,从节点不会立刻选举出一个新的主节点(有可能是网络的问题,或是重启),达到一定时间后主从机还没有取得联系就认为主节点宕机了,从节点从新选举出一个新的主节点,并修改sentinel.conf 配置文件,其余节点追随这个新选出来的节点(就算原主节点恢复功能,任然会被当做子节点)。
主从并未解决容量问题:
增加容量方案:
方案1(数据可以分类): 客户端融入逻辑代码
方案2(数据不可分类):客户端将每一笔数据(key)进行分片
方案3(数据不可分类):数据随机扔到一个redis,有一个redis进行消费(类似于kafuka)
方案4(数据不可分类):一致性hash算法,没有取模,采用hash环,增加一个物理节点是可能会导致数据丢失
全局洗牌: 新增节点,在方案二下全部数据都需要重新取模计算
虚拟节点: 一个物理节点可以对应多个逻辑节点,key先找到最近的虚拟节点,在由虚拟节点去找物理节点。(解决:数据倾斜的问题(数据都放这一个物理节点附近))
客户端/服务端的物理连接图:
- server端连接成本高:客户端负责逻辑实现
2.2.添加代理(类似于nginx) 代理层负责逻辑实现
3.流量过大再加 LVS
keepalived 作用 :
- 检测代理的健康状态
- 监控主备LVS状态
分片解决hash环:(解决:当新增加一个节点时,hash方式的全部重新hash)
增加一个mapping ,分成多个槽位,例:hash值为0 1 2的都映射到redis1实例
redis的做法(无主模型): 当redis要找k1是,假设k1在redis3中。redis客户端先是选择了redis2实例,每个redis实例都有一样的hash算法,计算k1的hash值后与自己的mapping值和其他redis实例的mapping值对比,发现在redis3的mapping值有对应的值,返回给客户端应该重定向到redis3
redis代理 : twemproxy / predixy (可以代理一个或多个redis实例,对外显示只有一个服务端,对内可以是redis集群。不同key会被存储到不同的redis实例)
只能在同一个组才能执行事务
创建redis集群:
redis-cli --cluster create 127.0.0.1:30001 127.0.0.1:30002 127.0.0.1:30003 127.0.0.1:30004 127.0.0.1:30005 127.0.0.1:30006 1 //父本的数量为1 从也为1,一共3个组。创建后共有3个master 3个slave
缓存出现的问题:
1.缓存穿透
缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大
解决方案 :
- 布隆过滤器
- 非法请求过滤,黑名单
- 缓存空值或默认值
2.缓存击穿
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。
解决方案:
方案一:
后台刷新:后台启动一个常驻进程,用于在缓存失效前主动更新缓存中的数据,在缓存失效前后台进程刷新一下缓存中的数据,保证数据永远不会过期。
缺点:会增加系统难度,比较适合那些key相对固定,cache力度较大的业务。若是key比较分散则不太合适,实现起来也比较复杂。
方案二:
get请求主动更新缓存:将缓存key的过期时间点一起保存到缓存里(这个方法有很多,拼接,添加新字段,单独key皆可),在每次执行get操作后,都将get出来的缓存过期时间和当前系统时间做一次对比,如果缓存时间减去当前系统时间小于等于某个设定值后,则主动更新缓存。这样就能保证缓存中的数据始终是最新的。
缺点:在某个特殊时刻时,例如在缓存即将过期时没有get请求,导致缓存已经过期,恰好此时有大量并发请求过来,那就惨了。当然这种情况比较极端,但也是有可能的。
方案三:
分级缓存:采用L1(一级缓存)和L2(二级缓存)的缓存方式,L1缓存失效时间短,L2缓存失效时间长。请求优先从L1获取数据,如果L1未命中则加锁,只有1个线程获取到锁,这个线程再从数据库中读取数据并将数据更新到L1和L2中,而其他线程依旧从L2缓存中获取数据并返回。
缺点:这种方式主要是通过避免缓存同时失效并结合锁机制实现。那么就会出现一个问题,有一瞬间L2可能会存在脏数据。就是当数据更新时,L1的缓存被淘汰,但是L2未被淘汰。且这种方案可能会造成额外的缓存空间浪费。
方案四:
加锁:
static Lock reenLock = new ReentrantLock();
public List<String> getData04() throws InterruptedException {
List<String> result = new ArrayList<String>();
// 从缓存读取数据
result = getDataFromCache();
if (result.isEmpty()) {
if (reenLock.tryLock()) {
try {
System.out.println("我拿到锁了,从DB获取数据库后写入缓存");
// 从数据库查询数据
result = getDataFromDB();
// 将查询到的数据写入缓存
setDataToCache(result);
} finally {
reenLock.unlock();// 释放锁
}
} else {
result = getDataFromCache();// 先查一下缓存
if (result.isEmpty()) {
System.out.println("我没拿到锁,缓存也没数据,先小憩一下");
Thread.sleep(100);// 小憩一会儿
return getData04();// 重试
}
}
}
return result;
}
3.缓存雪崩
缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是, 缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
解决方案: 预防为主
1.对于服务不可访问来说
想要一个高可用的服务,首先我们就要集群使用,即使有redis挂了,依然有其他的redis可以过来提供服务,当然最好就是使用redis cluster集群,多个redis master,不仅有主备服务可以在故障时进行切换,即使真的主备都挂了,那也只是一部分数据不能提供服务了,还有其他的master可以给其他提供服务,尽量的减少redis宕机所带来的影响
2.对于大量key同时失效来说
最主要的,就是分散key的失效时间,比如说在插入redis缓存的时候,指定key的失效时间为在一段范围区间内的随机值
linux下部署redis的技术选型:
10万并发量 主从复用
100万并发量 redis集群