本文内容绝大部分来自狂神的课堂笔记,笔者只是加以整理和拓展,方便大家学习。
参考教程:【狂神说Java】Redis最新超详细版教程通俗易懂
参考博文:狂神redis学习笔记-整理
在90年代,一个基本的网站访问量一般不会太大,单个数据库已经绰绰有余。那时使用的更多是静态网页(html),服务器不会有太大的压力。
在这种情况下,网站的瓶颈:
数据量太大,一个机器放不下数据的索引 (例如B+Tree),一个机器内存放不下访问量太大(读写混合),一个服务器承受不了只要开始出现以上三种情况之一,那么就必须想办法进行优化。
拓展阅读:B-Tree和B+Tree详解
网站80%的情况都是在读,而每次都要去查询数据库的话就十分的麻烦,所以为了减轻数据的压力,我们可以使用缓存来保证效率。
优化过程:优化数据结构和索引–> 文件缓存(IO)–> Memcached(当时最热门的技术)
早些年MyISAM有表锁,但十分影响效率,高并发下就会出现严重的锁问题。转战Innodb(行锁)。慢慢的就开始使用分库分表来解决写的压力。MySQL在那个年代推出了表分区,但并没有多少公司使用。MySQL的集群,能够很好的满足那个年代所有的需求。
在数据量很大、变化很快的情况下,MySQL等关系型数据库不够用了。有的人使用它来存一些比较大的文件,比如博客和图片。数据库表很大,效率自然就低了。如果有一种数据库专门来处理这种数据,MySQL的压力就会变得十分小,因为在大数据的IO压力下,表几乎没法更改。
NoSQL = Not Only SQL (不仅仅是SQL),非关系型数据库。
随着web2.0互联网的诞生!传统的关系型数据库很难对付web2.0时代,尤其是超大规模的高并发的社区,暴露出很多难以克服的问题,NoSQL在当今大数据环境下发展的十分迅速,Redis是发展最快的,而且是我们当下必须要掌握的一个技术。
很多的数据类型比如用户的个人信息、社交网络和地理位置。这些数据类型的存储不需要一个固定的格式,也不需要多余的操作,那就可以横向扩展了。
拓展阅读:CAP原则(CAP定理)、BASE理论
大厂采用策略:
新浪:Redis美团:Redis + Tair阿里和百度:Redis + MemecacheMongoDB是一个基于分布式文件存储的数据库,C++编写,主要用来处理大量的文档。MongoDB是一个介于关系型数据库和非关系型数据库中间的产品。MongoDB是非关系型数据库中功能最丰富,最像关系型数据库的一个。
HBase 分布式文件系统
它不是存图形,放的是关系,应用于朋友圈社交网络和广告推荐等。有Neo4j和InfoGrid等。
此为社交拓扑图
Redis(Remote Dictionary Server ),即远程字典服务,是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。
Redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。
免费和开源,是当下最热门的NoSQL技术之一,也被人们称之为结构化数据库。
简而言之,Redis可用作数据库、缓存和消息中间件MQ。
Redis官方推荐在Linux服务器上搭建。
Redis是单线程的,它很快,基于内存操作。CPU不是Redis的性能瓶颈,Redis的瓶颈是机器的内存和网络带宽。
Redis是C语言写的,官方提供的数据为100000+的 QPS,完全不比同样是使用key-value的Memcache差。
Q:Redis为什么单线程还这么快?
误区1:高性能的服务器一定是多线程的误区2:多线程(CPU上下文会切换)一定比单线程效率高speed:CPU>内存>硬盘
核心:Redis是将所有的数据全部放到内存中的,所以说使用单线程去操作效率就是最高的,多线程(CPU上下文会切换:耗时的操作),对于内存系统来说,如果没有上下文切换效率就是最高的。多次读写都是在一个CPU上的,在内存情况下,这个就是最佳的方案。
Q:Redis-server闪退? A:Windows下双击redis-server.exe会闪退,笔者目前没有找到解决方法,只能通过cmd曲线救国…
注意:本机使用的redis.conf配置文件在usr/local/bin/myredisconfig下,修改配置文件后在bin目录下启动方式为redis-server /myredisconfig/redis.conf。 关闭服务:redis-cli shutdown
Redis有16个数据库,默认使用第0个,可以使用select进行切换。(eg:select 3)
keys * 查看数据库所有的key flushdb 清空当前数据库 flushall 清空所有数据库 set [key] [value] 设置键值对,重复设置一个键会将原来的值给覆盖掉 get [key] 通过键名获取值 exist [key] 是否存在键 move [key] [0-15] 将键移动到某个数据库 expire [key] [time] 将键设置为n秒后过期 ttl [key] 查看键剩余存活时间 type [key] 查看键类型特点:值有序,可重复。
lpush [listname] [value] 往列表头部插入数据 lrange [listname] [range(0 1)] 获取列表中某个范围的值(获取列表中第0和第1个值) rpush [listname] [value] 往列表尾部插入数据 lpop [listname] 移除列表头元素 rpop [listname] 移除列表尾元素 lindex [listname] [index] 通过下标获取值 llen [listname] 获取列表长度 lrem [listname] [count] [value] 移除n个值 ltrim [listname] [range(1 2)] 截取范围内值(截取列表中第1和第2个值)(注:原list将被改变,未被截取到的数据将被删除) rpoplpush [source] [destination] 移除列表的最后一个元素并将其插入另一个列表的头部 lset [listname] [index] [value] 修改下标值 exist [listname] 判断某列表是否存在 linsert [listname] [before|after] [value](参考值) [value](待插入的值) 在某个值的前面/后面插入值注意:"l"只有在push和pop命令才代表左右的意思,其余情况均为list的意思! 注意一下取出value的顺序 ↑
特点:值无序,不可重复。
所谓无序,是在改动元素后整个set集合会重新排序;若不改动元素,set集合会一直按照某种固定顺序存数。
sadd [setname] [value] 往集合中添加值 smembers [setname] 查看某集合 sismember [setname] [value] 判断集合中是否存在该值(返回0表示不存在,返回1表示存在) scard [setname] 获取集合元素个数 srem [setname] [value] 移除集合中的指定元素 srandmember [setname] 随机取出一个元素 srandmember [setname] [count] 随机取出若干个元素 spop [setname] 移除一个元素 smove [source] [destination] [value] 移除列表的一个指定元素并将其添加到另一个集合中 sdiff [set1] [set2] 找出属于set1但不属于set2的元素(求差集) 差集:记A,B是两个集合,则所有属于A且不属于B的元素构成的集合,叫做集合A减集合B(或集合A与集合B之差),类似地,对于集合A、B,把集合{x∣x∈A,且x∉B}叫做A与B的差集。记作:B-A sinter [set1] [set2] 求两个集合共有的元素(求交集) sunion [set1] [set2] 求两个集合出现的所有元素(求并集)特点:map集合
hset [hashname] [key] [value] 存值 hget [hashname] [key] 取值 hmset [hashname] [key1] [value1] [key2] [value2]...... 批量存值 hmget [hashname] [key1] [key2]...... 批量取值 hgetall [hashname] 获取所有键值对 hdel [hashname] [key] 删除指定键值对 hlen [hashname] 获取哈希表长度(即有几个键值对) hexist [hashname] [key] 判断表中键值对是否存在(返回0表示不存在,返回1表示存在) hkeys [hashname] 获取表中所有的key hvals [hashname] 获取表中所有的value hincrby [hashname] [key] [increase] 将key对应的value增加若干大小(可通过负数实现减少) hsetnx [hashname] [key] [value] 若键不存在,创建并设置值特点:通过带上分数(score)属性可以做到值有序
zadd [setname] [score] [value] 往集合中添加值 zrange [setname] [range(0 1)] 获取集合中某个范围的值(获取集合中第0和第1个值) zrangebyscore [setname] -inf +inf 按分数从小到大获取集合中的值 zrevrangebyscore [setname] 0 -1 按分数从大到小获取集合中的值 zrangebyscore [setname] -inf +inf withscores 按分数从小到大获取集合中的值并显示分数 zrem [setname] [value] 移除集合中的指定元素 zcard [setname] 获取集合元素个数 zcount [setname] [scorerange(1 3)] 某分数范围内的值的个数(分数在[1,3]范围内的值的个数)geo底层的实现原理其实就是Zset,可以用Zset命令来操作geo,比如用 zrange china:city 0 -1 来查看所有中国的城市。
它是用作基数统计的算法。基数计算(cardinality counting)指的是统计一批数据中的不重复元素的个数。例如A{1,3,5,7,8,7},B{1,3,5,7,8},基数=5 拓展阅读:解读Cardinality Estimation算法(第一部分:基本概念)
通常网页的UV(UV(Unique visitor)是指通过互联网访问、浏览这个网页的自然人),一个人访问一个网站多次,访问量还是算作一次。
传统的方式,set保存用户的id,然后就可以统计set中的元素数量作为标准判断,这个方式如果保存大量的用户id,就会比较麻烦。我们的目的是为了计数,而不是保存用户id。
占用的内存是固定的,264个不同元素的数据,只需要废12KB内存。所以如果要从内存角度来比较的话,Hyperloglog是首选。
注意:若允许容错(Hyperloglog错误率为0.81%),推荐使用Hyperloglog;若不允许容错,那就用set或者自己的数据类型即可。
位存储,操作二进制位来进行记录,只有0和1两个状态。 统计用户信息:活跃不活跃,登录未登录,365天打卡…拥有两个状态的,都可以用Bitmap。 使用Bitmap记录一周打卡情况(0-6周一到周日,0未打卡1已打卡)↑
setbit [key] [offset] [value] 存值 getbit [key] [offset] 取值 bitcount [key] 统计1的个数Redis事务的本质:一组命令的集合,一个事务中的所有命令都会被顺序化,在事务执行的过程中,会按照顺序执行。一次性、顺序性、排他性,执行一系列的命令。
所有的命令在事务中,并没有直接被执行,只有发起执行命令的时候才会执行。
Redis单条命令是保证原子性的,但是事务不保证原子性,并且没用隔离级别的概念。
Redis的事务流程:
开启事务(multi)命令入队(…)执行事务(exec)正常执行事务 ↑ 放弃事务 ↑
编译型异常(代码有问题,命令有错),事务中所有的命令都不会被执行。 运行时异常(例如字符串自增),如果事务队列中不存在语法性错误,那么执行命令的时候,其他命令是可以正常执行的,只有错误部分会抛出异常,也就是说错误并不影响其他命令的执行。
很乐观,认为什么时候都不会出问题,所以不会上锁,更新数据的时候去判断一下,在此期间是否有人修改过这个数据,若有人中途修改,事务会执行失败(事务执行失败后需要unwatch再watch重新开启监控)
认为什么时候都会出问题,无论做什么都会加锁。
redis官方推荐的Java连接开发工具,使用Java操作redis中间件,如果要用Java操作redis,那么一定要对Jedis十分的熟悉。
所有的API命令,就是我们对应的上面学习的指令,一个都没有变化,只是换成了方法而已
SpringBoot操作数据:spring-data jpa jdbc mongodb redis
SpringData也是和SpringBoot齐名的项目
说明:在SpringBoot2.x之后,原来使用的jedis被替换成了lettuce
jedis:采用的是直连,多个线程操作的话是不安全的,若想要避免不安全,使用jedis pool连接池,更像BIO模式。
lettuce:采用netty,实例可以在多个线程中进行共享,不存在线程不安全的情况,可以减少线程数量,更像NIO模式。
redis启动的时候,通过配置文件启动。
面试和工作,持久化都是重点 redis是内存数据库,如果不讲内存中的数据状态保存到硬盘,那么一旦服务器进程退出,服务器中的数据库状态也会消失。所以redis提供了持久化功能。
在主从复制中,rdb就是备用的,在从机上面。
将我们的所有命令都记录下来,history,恢复的时候就把这个文件全部再执行一遍。
aof 默认就是文件的无限追加,文件会越来越大。 如果 aof 文件大于 64m,太大了! fork一个新的进程来将我们的文件进行重写。
Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。例如微信和微博的关注系统。 Redis 客户端可以订阅任意数量的频道。 订阅/发布消息图: 消息发送者 + 频道 + 消息订阅者 下图展示了频道 channel1 , 以及订阅这个频道的三个客户端 —— client2 、 client5 和 client1 之间的关系: 当有新消息通过 PUBLISH 命令发送给频道 channel1 时,这个消息就会被发送给订阅它的三个客户端:
主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(Master/Leader),后者称为从节点(Slave/Follower), 数据的复制是单向的,只能由主节点复制到从节点(主节点以写为主、从节点以读为主)。
默认情况下,每台Redis服务器都是主节点,一个主节点可以有0个或者多个从节点,但每个从节点只能由一个主节点。
info replication 查看当前库的信息
127.0.0.1:6379> info replication # Replication role:master # 角色 connected_slaves:0 # 从机数量 master_replid:3b54deef5b7b7b7f7dd8acefa23be48879b4fcff master_replid2:0000000000000000000000000000000000000000 master_repl_offset:0 second_repl_offset:-1 repl_backlog_active:0 repl_backlog_size:1048576 repl_backlog_first_byte_offset:0 repl_backlog_histlen:0既然需要启动多个服务,就需要多个配置文件。每个配置文件对应修改以下信息:
端口号pid文件名日志文件名rdb文件名启动单机多服务集群: 一主二从配置: 默认情况下,每台Redis服务器都是主节点 ;我们一般情况下只用配置从机就好了。 认老大,一主(79)二从(80,81)。 使用SLAVEOF host port就可以为从机配置主机了。 然后主机上也能看到从机的状态: 我们这里是使用命令搭建,是暂时的,真实开发中应该在从机的配置文件中进行配置,这样的话是永久的 使用规则
从机只能读,不能写,主机可读可写但是多用于写。 127.0.0.1:6381> set name sakura # 从机6381写入失败 (error) READONLY You can't write against a read only replica. 127.0.0.1:6380> set name sakura # 从机6380写入失败 (error) READONLY You can't write against a read only replica. 127.0.0.1:6379> set name sakura OK 127.0.0.1:6379> get name "sakura" 当主机断电宕机后,默认情况下从机的角色不会发生变化 ,集群中只是失去了写操作,当主机恢复以后,又会连接上从机恢复原状。当从机断电宕机后,若不是使用配置文件配置的从机,再次启动后作为主机是无法获取之前主机的数据的,若此时重新配置称为从机,又可以获取到主机的所有数据。这里就要提到一个复制原理。 默认情况下,主机故障后,不会出现新的主机,有两种方式可以产生新的主机: 从机手动执行命令slaveof no one,这样执行以后从机会独立出来成为一个主机使用哨兵模式(自动选举)Q:如果没有老大了,这个时候能不能选择出来一个老大呢? A:手动。如果主机断开了连接,我们可以使用SLAVEOF no one让自己变成主机,其他的节点就可以手动连接到最新的主节点(手动)。如果这个时候老大修复了,那么就重新连接。
更多信息参考博客:Redis哨兵(Sentinel)模式
主从切换技术的方法是:当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用。这不是一种推荐的方式,更多时候,我们优先考虑 哨兵模式。
这里的哨兵有两个作用:
通过发送命令,让Redis服务器返回监控其运行状态,包括主服务器和从服务器。当哨兵监测到master宕机,会自动将slave切换成master,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机。然而一个哨兵进程对Redis服务器进行监控,可能会出现问题,为此,我们可以使用多个哨兵进行监控。各个哨兵之间还会进行监控,这样就形成了多哨兵模式。
假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行failover过程,仅仅是哨兵1主观的认为主服务器不可用,这个现象成为主观下线。当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行failover(故障转移)操作。切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线。
我们目前的状态是 一主二从。
配置哨兵配置文件 sentinel.conf #sentinel monitor 被监控的名称 host port 1 sentinel monitor myredis 127.0.0.1 6379 1后面的数字1,代表当主机挂了,1个哨兵认为master主节点失联,那么这时客观上认为主节点失联了。
启动哨兵 redis-sentinel xxx/sentinel.conf #成功启动哨兵模式此时哨兵监视着我们的主机6379,当我们断开主机后:
完整的哨兵模式配置文件 sentinel.conf
#Example sentinel.conf #哨兵sentinel实例运行的端口 默认26379 port 26379 #哨兵sentinel的工作目录 dir /tmp #哨兵sentinel监控的redis主节点的 ip port #master-name 可以自己命名的主节点名字 只能由字母A-z、数字0-9 、这三个字符".-_"组成。 #quorum 当这些quorum个数sentinel哨兵认为master主节点失联 那么这时 客观上认为主节点失联了 #sentinel monitor <master-name> <ip> <redis-port> <quorum> sentinel monitor mymaster 127.0.0.1 6379 1 #当在Redis实例中开启了requirepass foobared 授权密码 这样所有连接Redis实例的客户端都要提供密码 #设置哨兵sentinel 连接主从的密码 注意必须为主从设置一样的验证密码 #sentinel auth-pass <master-name> <password> sentinel auth-pass mymaster MySUPER--secret-0123passw0rd #指定多少毫秒之后 主节点没有应答哨兵sentinel 此时 哨兵主观上认为主节点下线 默认30秒 #sentinel down-after-milliseconds <master-name> <milliseconds> sentinel down-after-milliseconds mymaster 30000 #这个配置项指定了在发生failover主备切换时最多可以有多少个slave同时对新的master进行 同步, 这个数字越小,完成failover所需的时间就越长, 但是如果这个数字越大,就意味着越 多的slave因为replication而不可用。 可以通过将这个值设为 1 来保证每次只有一个slave 处于不能处理命令请求的状态。 #sentinel parallel-syncs <master-name> <numslaves> sentinel parallel-syncs mymaster 1 #故障转移的超时时间 failover-timeout 可以用在以下这些方面: #1. 同一个sentinel对同一个master两次failover之间的间隔时间。 #2. 当一个slave从一个错误的master那里同步数据开始计算时间。直到slave被纠正为向正确的master那里同步数据时。 #3.当想要取消一个正在进行的failover所需要的时间。 #4.当进行failover时,配置所有slaves指向新的master所需的最大时间。不过,即使过了这个超时,slaves依然会被正确配置为指向master,但是就不按parallel-syncs所配置的规则来了 #默认三分钟 #sentinel failover-timeout <master-name> <milliseconds> sentinel failover-timeout mymaster 180000 #SCRIPTS EXECUTION #配置当某一事件发生时所需要执行的脚本,可以通过脚本来通知管理员,例如当系统运行不正常时发邮件通知相关人员。 #对于脚本的运行结果有以下规则: #若脚本执行后返回1,那么该脚本稍后将会被再次执行,重复次数目前默认为10 #若脚本执行后返回2,或者比2更高的一个返回值,脚本将不会重复执行。 #如果脚本在执行过程中由于收到系统中断信号被终止了,则同返回值为1时的行为相同。 #一个脚本的最大执行时间为60s,如果超过这个时间,脚本将会被一个SIGKILL信号终止,之后重新执行。 #通知型脚本:当sentinel有任何警告级别的事件发生时(比如说redis实例的主观失效和客观失效等等),将会去调用这个脚本, #这时这个脚本应该通过邮件,SMS等方式去通知系统管理员关于系统不正常运行的信息。调用该脚本时,将传给脚本两个参数, #一个是事件的类型, #一个是事件的描述。 #如果sentinel.conf配置文件中配置了这个脚本路径,那么必须保证这个脚本存在于这个路径,并且是可执行的,否则sentinel无法正常启动成功。 #通知脚本 #sentinel notification-script <master-name> <script-path> sentinel notification-script mymaster /var/redis/notify.sh #客户端重新配置主节点参数脚本 #当一个master由于failover而发生改变时,这个脚本将会被调用,通知相关的客户端关于master地址已经发生改变的信息。 #以下参数将会在调用脚本时传给脚本: #<master-name> <role> <state> <from-ip> <from-port> <to-ip> <to-port> #目前<state>总是“failover”, #<role>是“leader”或者“observer”中的一个。 #参数 from-ip, from-port, to-ip, to-port是用来和旧的master和新的master(即旧的slave)通信的 #这个脚本应该是通用的,能被多次调用,不是针对性的。 #sentinel client-reconfig-script <master-name> <script-path> sentinel client-reconfig-script mymaster /var/redis/reconfig.sh大量的key设置了相同的过期时间,导致在缓存在同一时刻全部失效,造成瞬时DB请求量大、压力骤增,引起雪崩。
这个思想的含义是,既然redis有可能挂掉,那就增设几台redis,这样一台挂掉之后其他的还可以继续工作,其实就是搭建集群。
这个解决方案的思想是,在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。