1.概念
Redis(Remote Dictionary Server) 是一个使用 C 语言编写的,开源的(BSD许可)高性能非关系型(NoSQL)的键值对数据库。
传统数据库遵循 ACID 规则,而 NoSQL(Not Only SQL ) 一般为分布式,而分布式一般遵循 CAP 定理。
NoSql类型
- 键值(key-value)存储数据库 redis
- 列存储数据库:键仍然存在,但是指向了多个列,HBase (博客(标签和文章),日志)、Cassandra
- 文档型数据库 MongoDb (淘宝商品的评价)、CouchDB
- 图形数据库(专注于构建关系图谱) Neo4j (好友列表)、InfoGrid
Redis 可以存储键和五种不同类型的值之间的映射。键的类型只能为字符串,值支持五种数据类型:字符串、列表、集合、散列表、有序集合。
为什么redis快?
- 完全基于内存,绝大部分请求是纯粹的内存操作
- 采用单线程,避免了不必要的上下文切换和竞争条件
- 使用多路I/O复用模型,非阻塞IO
- 数据结构简单,对数据操作也简单,Redis中数据结构是专门进行设计的
优缺点
- 读写性能优异
- 数据结构丰富
- 支持数据持久化、支持事务
- 支持主从复制,主机会自动将数据同步到从机,可以进行读写分离
- 物理内存限制,不能用作海量数据的高性能读写
- 在线扩容复杂,不具备自动容错和恢复功能。主从机宕机会导致前端部分读写请求失败,部分数据未能及时同步到从机,引起数据不一致问题,降低了系统可用性
场景
- 缓存。将热点数据放到内存中,设置内存的最大使用量以及淘汰策略来保证缓存的命中率
- 会话缓存。 Redis 来统一存储多台应用服务器的会话信息
- 消息队列。不过最好使用 Kafka、RabbitMQ 等消息中间件
- 分布式锁。 Redis 自带的 SETNX 命令实现分布式锁,还可以使用官方提供的 RedLock 分布式锁
- 计数器。对 String 进行自增自减运算,从而实现计数器功能。Redis 这种内存型数据库的读写性能非常高,很适合存储频繁读写的计数量
2.数据类型
String
- 简单的 key-value 类型,一个键最大能存储512MB
- string 是二进制安全的,可以包含任何数据。比如字符串、整型、浮点数、序列化对象、图片
```shell set key value #设置单个值 get key #获取指定key mset key1 value1 key2 value2 key3 value3 #批量设置 mget key1 key2 key3 #批量获取
incr/decr key #递增/递减key incrby/decrby key increment #递增/递减 key指定的整数 del key1 #删除key exists key #判断是否存在key strlen key #获取字符串长度
EX:key 在多少秒之后过期;PX:key 在多少毫秒之后过期;NX:当 key 不存在的时候,才创建 key,效果等同于setnx key value;XX:当 key 存在的时候,覆盖 key
set key value [Ex seconds][PX milliseconds][NX|XX] setnx key value #不存在就插入(not exists) setex key time value #过期时间(expire) expire key 10 #10s key过期
getrange key start end #获取key中字符串的子字符串,从start开始,end结束 setrange key index value #从index开始往后的value getrange name 0 -1 #字符串分段 getset name new_cxx #设置值,返回旧值,当key不存在,返回nil append key value #字符串拼接,追加至末尾,如果不存在,为其赋值
<a name="3dbf0c11"></a>#### 应用场景- **缓存功能:String**字符串是最常用的数据类型,因此用**Redis**作为缓存配合数据库作为存储层,利用**Redis**支持高并发的特点可以大大加快系统读写速度、以及降低后端数据库压力。- **计数器:**许多系统都会使用**Redis**作为系统的实时计数器,可以快速实现计数和查询。而且最终数据结果可以按照特定时间落地到数据库或者其它存储介质中进行永久保存。- **共享用户Session:**可以利用**Redis**将用户的**Session**集中管理,在这种模式只需要保证**Redis**的高可用,每次用户**Session**的更新和获取都可以快速完成。<a name="Hash"></a>### Hash- string类型的field和value的映射表,特别适合用于存储结构化对象(**这个对象没嵌套其他的对象**)如用户信息等- 类似 **Map** 的一种结构(Map<String,Map<Object,object>>),即以字符串为 key,以 Map 对象为 value```shellhset key field value #为指定的key设定field和valuehget key field #获取对象的字段值hmset myhash name cxx age 25 note "i am notes" #批量设置hmget myhash name age note #批量获取指定key的fieldhgetall myhash #返回hash表中所有字段和值hexists myhash name #在key里面是否存在指定的fieldhsetnx myhash score 100 #当不存在才创建该fieldhincrby myhash id 1 #增加某个field的值hdel myhash name age #删除一个或多个hash表的字段hkeys myhash #获取hash表所有字段hvals myhash #获取hash表所有值hlen myhash #获取hash表中的字段数量
应用场景
- 用户信息
- 购物车
- 新增商品:
hset shopcar:uid1024 334477 1 - 增加商品数量:
hincrby shopcar:uid1024 334477 1 - 商品总数:
hlen shopcar:uid1024 - 全部选择:
hgetall shopcar:uid1024
- 新增商品:
List
- 双端链表 List,有序,value可重复,通过下标取出对应value值,左右两边都能进行插入和删除数据
- lpush+lpop=Stack(栈)
- lpush+rpop=Queue(队列)
- lpush+ltrim=Capped Collection(有限集合)
- lpush+brpop=Message Queue(消息队列)
lpush mylist a b c #从左侧插入,右边的先出,相当于一个栈, eg:lpush list 1 2 3 lrange list 0 -1 输出:3 2 1 rpush mylist x y z #从右侧插入,左边的先出,eg:rpush list 1 2 3 lrange list 0 -1 输出:1 2 3 lrange mylist 0 -1 #获取列表指定范围的元素 lpushx key value #从左侧插入值,如果list不存在,则不操作 rpushx key value #从右侧插入值,如果list不存在,则不操作 lpop mylist #从左侧移除第一个元素 rpop mylist #移除列表最后一个元素 blpop key timeout #移除并获取列表第一个元素,如果列表没有元素会阻塞列表到等待超时或发现可弹出元素为止 brpop key timeout #移除并获取列表最后一个元素,如果列表没有元素会阻塞列表到等待超时或发现可弹出元素为止 llen mylist #获取列表长度 lrem mylist count value #删除指定个数的同一元素,eg:irem list 2 3 删掉了集合中的两个三 lindex mylist 2 #获取指定索引的元素,从零开始 lset key index value #指定索引的值 ltrim mylist 0 4 #对列表进行修改,让列表只保留指定区间的元素,不在指定区间的元素就会被删除eg:list1中元素1 2 3 4 5 ltrim list1 2 3 list1剩余元素:3 4 linsert mylist before|after a #在列表元素前或则后插入元素 rpoplpush list list2 #转移列表的数据 从右侧弹出,在左侧添加
应用场景
- 消息队列:Redis的链表结构可以轻松实现阻塞队列,可以使用左进右出的命令组成来完成队列的设计。比如数据的生产者可以通过Lpush命令从左边插入数据,多个数据消费者,可以使用BRpop命令阻塞的“抢”列表尾部的数据。
- 文章列表或者数据分页展示。列表不但有序同时还支持按照范围内获取元素,比如通过 lrange 命令,读取某个闭区间内的元素,基于 List 实现分页查询
Set
- string类型的无序不重复集合,不能通过索引下标获取元素
- 多个集合可以取交集、并集、差集
sadd key value1[value2] #向集合添加成员 smembers key #返回集合中所有成员 srem key member1 [member2] #移除集合中一个或多个成员 sismember key member #判断memeber元素是否是集合key成员的成员 scard key #返回集合成员数 sdiff | sinter | sunion key1 [key2] #集合间运算:返回所有集合的差集 | 交集 | 并集 srandmember key [count] #返回集合中一个或多个随机数 spop key #移除并返回集合中的一个随机元素 smove source destination member #将member元素从source集合移动到destination集合 sdiffstore destination key1[key2] #返回给定的第一个集合与其他的集合的差集并存储在destination中eg:set1:1 2 3 set2:3 4 5 6 ;sdiffstore set3 set1 set2 ;smembers set3 ;result:1 2
应用场景
- 实现共同好友
- SINTER 我关注的人 Ta关注的人
- 统计访问网站的所有独立 IP
- 抽奖小程序
- 某个用户点击了立即参与按钮:sadd key useId
- 显示已经有多少人参与了抽奖:SCARD key
- 随机抽奖2个人,元素不删除:SRANDMEMBER key 2
- 随机抽奖3个人,元素会删除:SPOP key 3
- 朋友圈点赞
- 新增点赞:SADD pub:msgID 点赞用户ID1 点赞用户ID2
- 取消点赞:SREM pub:msgID 点赞用户ID
- 展现所有点赞过的用户:SMEMBERS pub:msgID
- 点赞用户数统计:SCARD pub.msgID
- 判断某个朋友是否对楼主点赞过:SISMEMBER pub:msgID 用户ID
zset
- string类型元素的不允许重复集合,每个元素都会关联一个double类型的分数(排序的依据),zset的成员是唯一的,但分数(score)却可以重复
- 排行榜
zadd zset 1 one zadd zset 2 two zadd zset 3 three zincrby zset 1 one #增长分数 zscore zset two #获取分数 zrange zset 0 -1 withscores #定输出索引范围内的成员 zrangebyscore zset 10 25 withscores #指定输出score区间内的成员 zrangebyscore zset 10 25 withscores limit 1 2 #指定分页输出score区间内的成员 分数从高到低 Zrevrangebyscore zset 10 25 withscores #指定反向输出score区间内的成员 zcard zset #获取集合中的元素数量 zcount key min max #计算在有序集合中指定区间分数的成员数 Zrem zset one two #移除有序集合中的一个或多个成员 Zremrangebyrank zset 0 1 #移除有序集合中给定的索引区间的所有成员(第一名是0)(低到高排序) Zremrangebyscore zset 0 1 #移除有序集合中给定的分数区间的所有成员 Zrank zset one #返回有序集合指定成员的索引。分数最小的元素排名为0 Zrevrank zset one #返回有序集合指定成员的索引。分数最大的元素排名为0 ZINTERSTORE destination numkeys key [key …] #计算给定的一个或多个有序集的交集并将结果集存储在新的有序集合 destination 中,其中numkeys为集合个数 ZUNIONSTORE destination numkeys key [key …] #计算给定的一个或多个有序集的并集,并存储在新的 key 中
应用场景
- 排行榜:有序集合经典使用场景。例如视频网站需要对用户上传视频做排行榜,榜单维护可能是多维度:按照时间、按照播放量、按照获得的赞数等。
- 点击视频增加播放量:ZINCRBY hotvcr:20200919 1 八佰
- 展示当日排行前10条:ZREVRANGE hotvcr:20200919 0 9 WITHSCORES
- 做带权重的队列,比如普通消息的score为1,重要消息的score为2,然后工作线程可以选择按score的倒序来获取工作任务。让重要的任务优先执行。

Bitmap
位图是支持按 bit 位来存储信息,只有0和1两个状态,可以用来实现 布隆过滤器(BloomFilter)。
位图不是实际的数据类型,而是在String类型上定义的一组面向位的操作,使用1,0来表示数据,redis的key和value是有大小限制的,都是不能超过512M,那么bitmap最大能设置2^32的长度。
#字母b的ASCII码为98,转换成二进制为 01100010;字母i的ASCII码为105,转换成二进制为 01101001;字母g的ASCII码为103,转换成二进制为 01100111
set hello big
getbit hello 0 #b的二进制形式的第1位,即为0
getbit hello 1 #b的二进制形式的第2位,即为1
bitcount hello #1的个数:12
setbit hello 7 1 #把hello二进制形式的第8位设置为1,之前的ASCII码为98,现在改为99,即把b改为c
get hello #修改之后,获取'hello'的值,为'cig'
setbit hello 50 1 #setbit命令指定位大于目标长度,从第25到第49位中间用0来填充,第50位才会被设置为1 "cig\x00\x00\x00 "
统计用户信息 活跃,不活跃,登录,未登录,打卡,未打卡。
#1代表打卡,0代表未打卡
setbit sign 0 1
setbit sign 1 0
setbit sign 2 1
setbit sign 3 1
setbit sign 4 0
setbit sign 5 1
setbit sign 6 0
getbit sign 3 #周三是否打卡
bitcount sign #统计这周的打卡记录 (integer) 4
HyperLogLog
基于基数统计(不重复的数),占用内存是固定,2^64不同元素,只需要12kb内存。供不精确(0.81%的错误率)的去重计数功能,比较适合用来做大规模数据的去重统计,例如统计 UV(访客数量、每个用户一天只记录一次),PV(页面浏览量)。
pfadd mykey a b c d e f g #赋值mykey中元素有7个
pfcount mykey #统计大小
pfadd mykey2 a e r b c d r #赋值mykey2中元素有6个
pfmerge mykey3 mykey mykey2 #将两者的合并赋值给mykey3
pfcount mykey3 #(integer) 8
Geospatial
用来保存地理位置,并作位置距离计算或者根据半径计算位置等。底层原理是zset
geoadd cities:locations 116.28 39.55 beijing # 添加北京的经纬度
geoadd cities:locations 117.12 39.08 tianjin 114.29 38.02 shijiazhuang # 添加天津和石家庄的经纬度
geoadd cities:locations 118.01 39.38 tangshan 115.29 38.51 baoding # 添加唐山和保定的经纬度
geopos cities:locations tianjin #获取天津的地址位置信息
geodist cities:locations tianjin beijing km #获取两个地理位置的距离 "89.2061" m(米),km(千米),mi(英里),ft(尺)
geodist cities:locations tianjin baoding km #获取两个地理位置的距离 "170.8360"
#georedius key longitude latitude radiusm|km|ft|mi [withcoord] [withdist] [withhash] [COUNT count] [asc|desc] [store key][storedist key]
#withcoord:返回结果中包含经纬度
#withdist:返回结果中包含距离中心节点位置
#withhash:返回结果中包含geohash
#COUNT count:指定返回结果的数量
#asc|desc:返回结果按照距离中心节点的距离做升序或者降序
#store key:将返回结果的地理位置信息保存到指定键
#storedist key:将返回结果距离中心节点的距离保存到指定键
georadius cities:locations 110 30 1000 km #以给定经纬度为中心,寻找某一半径里的元素
georadius cities:locations 110 30 1000 km withdist #返回结果中包含距离中心节点位置
georadius cities:locations 110 30 1000 km withcoord #返回结果中包含经纬度
#georadiusbymember key member radiusm|km|ft|mi [withcoord] [withdist] [withhash] [COUNT count] [asc|desc] [store key][storedist key]
georadiusbymember cities:locations beijing 150 km #获取距离北京150km范围内的城市
zrem cities:locations baoding #geo的删除操作
3.常用命令
./redis-server #启动redis服务端
./redis-cli -h 127.0.0.1 -p 6379 #redis客户端连接redis服务端
#查看redis版本
./redis-server -v
info #sed_memory_human 表示实际已经占用的内存,maxmemory 表示 redis 最大占用内存
select 数据库ID #切换数据库
CONFIG GET requirepass #获取密码
CONFIG SET requirepass "1245678" #设置密码
CONFIG SET requirepass "" #设置密码为空
AUTH 12345678 #验证密码
#给客户端设置一个名称
client setname myclient1
client getname
#获取服务端口
config get port
redis-check-aof --fix appendonly.aof #将AOF文件中不符合规范的所有命令删除
OBJECT ENCODING key #查看key的类型
ttl key #以秒为单位返回key剩余时间,-1为永久,-2为失效
PTTL key #以毫秒为单位返回 key 的剩余的过期时间
key * #返回所有的键 使用keys命令会阻塞其他命令执行 keys命令一般不在生产环境中使用
dbsize #计算Redis中所有key的总数
dump key #序列化给定key,返回被序列化的值
rename key newkey #修改key的名称
move key db #移动key至指定数据库
type key #返回key所储存的值的类型 查看一个不存在的key时,返回None
randomkey #随机返回一个key
persist key #移除key的过期时间,key将持久保存
flushdb #清空当前库
flushall #通杀全部库
bgSave #后台异步保存数据到磁盘,会在当前目录下创建文件dump.rdb
save #同步保存数据到磁盘,会阻塞主进程,别的客户端无法连接
lastsave #命令获取最后一次成功执行快照的时间
CONFIG SET save "" #动态停止RDB保存规则
bgrewriteaof #开启重写
#slowlog get等命令定期将慢查询命令持久化到其他数据源 不管slowlog-max-len设置多大,当慢查询命令逐步增多时,最开始的慢查询命令会被丢掉
slowlog get [n] #获取慢查询队列
slowlog len #获取慢查询队列长度
slowlog reset #清空慢查询队列
注:命令不区分大小写,而key是区分大小写的,可使用 help @类型名词 查看
4.事务
Redis事务是通过MULTI、EXEC、WATCH等一组命令的集合。
三个阶段
- 事务开始 MULTI
- 命令入队,Redis不会执行这些命令
- 事务执行 EXEC,将执行所有命令
- 调用DISCARD,将刷新事务队列并退出事务
#事务中出现错误,全体失败
MULTI
SET k4 1
INCR k4 1 #(error) ERR wrong number of arguments for 'incr' command
EXEC
#事务中不出错误,错误语句执行失败,事务继续执行
MULTI
SET k5 1
INCR k5
SET k6 123456@qq.com
INCR k6
EXEC # OK、 (integer) 2、 OK 、ERR value is not an integer or out of range
#开启监听account_send 正常情况
WATCH account_send
MULTI
DECRBY account_send 100
INCRBY account_accept 100
EXEC #(integer) 900 (integer) 1100
#线程A 异常情况 redis中开启监听之后,就相当于给这个key加了一个锁(类似乐观锁),线程A去修改account_send的时候会拿到一个版本号,假设为1;但是线程A在事务还没提交的时候,线程B也去修改account_send的值,此时也会拿到一个版本号,假设为1,线程B执行结束,修改版本号为2,这是线程A开始执行事务中的代码,发现版本号为2,不是之前拿到的1,导致失败
WATCH account_send
MULTI
DECRBY account_send 100
INCRBY account_accept 100
EXEC #(nil)
#线程B在线程A开启监听和事务之后,执行EXEC之前,执行下面的操作篡改account_send的值,导致线程A执行失败
DECRBY account_send 100 #(integer) 800
INCRBY account_accept 100 #(integer) 1200
Redis 是单进程程序,并且它保证在执行事务时,不会对事务进行中断,事务可以运行直到执行完所有事务队列中的命令为止。
Redis中单条命令是原子性的,但事务不保证原子性,且没有回滚。事务中任意命令执行失败,其余的命令仍会被执行。
故Redis事务总是具有ACID中的一致性和隔离性,其他特性是不支持的。当服务器运行在AOF持久化模式下,并且appendfsync选项的值为always时,事务也具有持久性。
Redis事务其他实现
- 基于Lua脚本,Redis可以保证脚本内的命令一次性、按顺序地执行,
其同时也不提供事务运行错误的回滚,执行过程中如果部分命令运行错误,剩下的命令还是会继续运行完 - 基于中间标记变量,通过另外的标记变量来标识事务是否执行完成,读取数据时先读取该标记变量判断是否事务执行完成。但这样会需要额外写代码实现,比较繁琐
5.过期键的删除策略
Redis的过期策略就是指当Redis中缓存的key过期了,Redis如何处理。
定时过期
每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。
该策略可以立即清除过期数据,对内存友好;但是会占用大量CPU资源去处理过期数据,从而影响缓存响应时间和吞吐量
惰性过期
只有当访问一个key时,才会判断该key是否已过期,过期则清除。
该策略可以最大化地节省CPU资源,却对内存非常不友好;极端情况可能出现大量过期key没有再次被访问,占用大量内存。
定期过期
每隔一定的时间随机抽一些设置了过期时间的key,并清除其中已过期的key。
Redis中同时使用了惰性过期和定期过期两种过期策略。
6.内存淘汰策略
redis内存数据集大小上升到一定大小的时候,就会施行数据淘汰策略。一般剔除策略有 FIFO 淘汰最早数据、LRU 剔除最近最少使用、和 LFU 剔除最近使用频率最低。
全局的键空间选择性移除
- noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。默认
- allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。(这个是最常用的)
- allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。
- allkeys-lfu:对所有key使用LFU算法进行删除
设置过期时间的键空间选择性移除
- volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。
- volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。
- volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。
- volatile-lfu:对所有设置了过期时间的key使用LFU算法进行删除
LRU实现
LRU 算法核心是哈希链表,本质就是 HashMap+DoubleLinkedList 时间复杂度是O(1)。
//借助 JDK 自带的 LinkedHashMap
public class LRUCacheDemo<K, V> extends LinkedHashMap<K, V> {
// 缓存容量
private int capacity;
public LRUCacheDemo(int capacity) {
// accessOrder:the ordering mode. true for access-order;false for insertion-order
super(capacity, 0.75F, true);//true时,每次使用 key时(put或者get 时),都将key对应的数据移动到队尾(右边),表示最近经常使用;当false 时,key 的顺序为插入双向链表时的顺序
this.capacity = capacity;
}
// 用于判断是否需要删除最近最久未使用的节点
//
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return super.size() > capacity;
}
public static void main(String[] args) {
LRUCacheDemo lruCacheDemo = new LRUCacheDemo(3);
lruCacheDemo.put(1, "a");
lruCacheDemo.put(2, "b");
lruCacheDemo.put(3, "c");
System.out.println(lruCacheDemo.keySet());
lruCacheDemo.put(4, "d");
System.out.println(lruCacheDemo.keySet());
lruCacheDemo.put(3, "c");
System.out.println(lruCacheDemo.keySet());
lruCacheDemo.put(3, "c");
System.out.println(lruCacheDemo.keySet());
lruCacheDemo.put(3, "c");
System.out.println(lruCacheDemo.keySet());
lruCacheDemo.put(5, "x");
System.out.println(lruCacheDemo.keySet());
}
}

public class LRUCacheDemo {
// map 负责查找,构建一个虚拟的双向链表,它里面装的就是一个个 Node 节点,作为数据载体
// 1.构造一个node节点作为数据载体
class Node<K, V> {
K key;
V value;
Node<K, V> prev;
Node<K, V> next;
public Node() {
this.prev = this.next = null;
}
public Node(K key, V value) {
this.key = key;
this.value = value;
this.prev = this.next = null;
}
}
// 2.构建一个虚拟的双向链表,,里面安放的就是我们的Node
class DoubleLinkedList<K, V> {
Node<K, V> head;
Node<K, V> tail;
public DoubleLinkedList() {
head = new Node<>();
tail = new Node<>();
head.next = tail;
tail.prev = head;
}
// 3.添加到头
public void addHead(Node<K, V> node) {
node.next = head.next;
node.prev = head;
head.next.prev = node;
head.next = node;
}
// 4.删除节点
public void removeNode(Node<K, V> node) {
node.next.prev = node.prev;
node.prev.next = node.next;
node.prev = null;
node.next = null;
}
// 5.获得最后一个节点
public Node getLast() {
return tail.prev;
}
}
private int cacheSize;
Map<Integer, Node<Integer, Integer>> map;
DoubleLinkedList<Integer, Integer> doubleLinkedList;
public LRUCacheDemo(int cacheSize) {
this.cacheSize = cacheSize;//坑位
map = new HashMap<>();//查找
doubleLinkedList = new DoubleLinkedList<>();
}
public int get(int key) {
if (!map.containsKey(key)) {
return -1;
}
Node<Integer, Integer> node = map.get(key);
doubleLinkedList.removeNode(node);
doubleLinkedList.addHead(node);
return node.value;
}
public void put(int key, int value) {
if (map.containsKey(key)) { //update
Node<Integer, Integer> node = map.get(key);
node.value = value;
map.put(key, node);
doubleLinkedList.removeNode(node);
doubleLinkedList.addHead(node);
} else {
if (map.size() == cacheSize) //坑位满了
{
Node<Integer, Integer> lastNode = doubleLinkedList.getLast();
map.remove(lastNode.key);
doubleLinkedList.removeNode(lastNode);
}
//新增一个
Node<Integer, Integer> newNode = new Node<>(key, value);
map.put(key, newNode);
doubleLinkedList.addHead(newNode);
}
}
public static void main(String[] args) {
LRUCacheDemo lruCacheDemo = new LRUCacheDemo(3);
lruCacheDemo.put(1, 1);
lruCacheDemo.put(2, 2);
lruCacheDemo.put(3, 3);
System.out.println(lruCacheDemo.map.keySet());
lruCacheDemo.put(4, 1);
System.out.println(lruCacheDemo.map.keySet());
lruCacheDemo.put(3, 1);
System.out.println(lruCacheDemo.map.keySet());
lruCacheDemo.put(3, 1);
System.out.println(lruCacheDemo.map.keySet());
lruCacheDemo.put(3, 1);
System.out.println(lruCacheDemo.map.keySet());
lruCacheDemo.put(5, 1);
System.out.println(lruCacheDemo.map.keySet());
}
}
总结
Redis的内存淘汰策略的选取并不会影响过期的key的处理。内存淘汰策略用于处理内存不足时的需要申请额外空间的数据;过期策略用于处理过期的缓存数据。
如果达到设置的内存上限,Redis写命令返回错误信息(读命令还可正常返回)或者配置内存淘汰机制,当Redis达到内存上限时会冲刷掉旧内容。
7.缓存
缓存是高并发场景下提高热点数据访问性能的一个有效手段。缓存的类型分为:本地缓存、分布式缓存和多级缓存。
本地缓存
本地缓存是在进程内存中进行缓存,比如 JVM 堆中可以用 LRUMap 来实现,也可以使用 Ehcache 工具实现。
本地缓存是内存访问,没有远程交互开销,性能最好,但是受限于单机容量,一般缓存较小且无法扩展。
分布式缓存
分布式缓存一般都具有良好的水平扩展能力,对较大数据量的场景也能应付自如。缺点需要进行远程请求,性能不如本地缓存。
多级缓存
为平衡上述情况,实际业务中一般采用多级缓存。
本地缓存只保存访问频率最高的部分热点数据,其他的热点数据放在分布式缓存中。
缓存雪崩
缓存雪崩是指缓存同一时间大面积失效,所以后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。
解决方案
- 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生
- 一般并发量不是特别多的时候,使用最多的解决方案是加锁排队
- 给每一个缓存数据增加相应的缓存标记,记录缓存的是否失效,如果缓存标记失效,则更新数据缓存。
缓存击穿
缓存击穿是指缓存中没有但数据库中有数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。和缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
解决方案
- 设置热点数据永远不过期
- 加互斥锁,互斥锁
缓存穿透
缓存穿透是指缓存和数据库中都没有数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。
解决方案
- 接口层增加校验,如参数做校验,不合法的参数直接代码return
- 将key-value对写为key-null,缓存有效时间可以设置短点
- 采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免对底层存储系统的查询压力
布隆过滤器(推荐)
Bloom-Filter算法的核心思想是引入了k(k>1)k(k>1)个相互独立的哈希函数,利用多个不同的Hash函数来解决“冲突”。保证在给定的空间、误判率下,完成元素判重的过程。
当一个元素被加入集合时,通过K个散列函数将这个元素映射成一个位数组中的K个点,把它们置为1。检索时,我们只要看看这些点是不是都是1就(大约)知道集合中有没有它了:如果这些点有任何一个0,则被检元素一定不在;如果都是1,则被检元素很可能在。
Bloom-Filter一般用于在大数据量的集合中判定某元素是否存在。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。

要使用BloomFilter,需要引入guava包:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>23.0</version>
</dependency>
public class TestBloomFilter {
private static int total = 1000000;
private static BloomFilter<Integer> bf = BloomFilter.create(Funnels.integerFunnel(), total);
public static void main(String[] args) {
// 初始化1000000条数据到过滤器中
for (int i = 0; i < total; i++) {
bf.put(i);
}
// 匹配已在过滤器中的值,是否有匹配不上的
for (int i = 0; i < total; i++) {
if (!bf.mightContain(i)) {
System.out.println("不包含");
}
}
// 匹配不在过滤器中的10000个值,有多少匹配出来
int count = 0;
for (int i = total; i < total + 10000; i++) {
if (bf.mightContain(i)) {
count++;
}
}
System.out.println("误伤的数量:" + count);
}
}
缓存预热
缓存预热是指系统上线后,将相关的缓存数据直接加载到缓存系统。用户直接查询事先被预热的缓存数据。
解决方案
- 数据量不大,可以在项目启动的时候自动进行加载
- 定时刷新缓存
缓存降级
当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务是可用的。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。
缓存降级目的是保证核心服务可用,即使是有损的。
在进行降级之前要对系统进行梳理,哪些必须誓死保护,哪些可降级(参考日志级别设置预案):
- 一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级
- 警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警
- 错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级
- 严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级
缓存与数据库双写时的数据一致性
Cache Aside Pattern
- 读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。
- 更新的时候,先更新数据库,然后再删除缓存。
8.线程模型
Redis 采用单线程模式处理请求。这样做的原因有 2 个:一个是因为采用了非阻塞的异步事件处理机制;另一个是缓存数据都是内存操作 IO 时间不会太长,单线程可以避免线程上下文切换产生的代价。
Redis基于Reactor模式开发了网络事件处理器,这个处理器被称为文件事件处理器(file event handler)。它的组成结构为4部分:多个套接字、IO多路复用程序、文件事件分派器、事件处理器。因为文件事件分派器队列的消费是单线程的,所以Redis才叫单线程模型。
- 文件事件处理器使用 I/O 多路复用(multiplexing)程序来同时监听多个套接字, 并根据套接字目前执行的任务来为套接字关联不同的事件处理器。
- 当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时, 与操作相对应的文件事件就会产生, 这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。
虽然文件事件处理器以单线程方式运行, 但通过使用 I/O 多路复用程序来监听多个套接字, 文件事件处理器既实现了高性能的网络通信模型, 又可以很好地与 redis 服务器中其他同样以单线程方式运行的模块进行对接, 这保持了 Redis 内部单线程设计的简单性。
9.配置文件
# 指定包含其他配置文件,可以在同一主机上多个redis实例之间使用同一份配置文件,
# 而同时各个实例又拥有自己的特定配置文件
include /path/to/local.conf
#是否在后台运行;no不是后台运行
daemonize yes
#是否开启保护模式,默认开启。要是配置里没有指定bind和密码。开启该参数后,redis只会本地进行访问,拒绝外部访问。
protected-mode yes
#当 Redis 以守护进程方式运行时,Redis 默认会把 pid 写入 /var/run/redis.pid 文件
pidfile /var/run/redis/redis-server.pid
#redis监听的端口号
port 6379
#此参数确定了TCP连接中已完成队列(完成三次握手之后)的长度, 当然此值必须不大于Linux系统定义的/proc/sys/net/core/somaxconn值,默认是511,而Linux的默认参数值是128。当系统并发量大并且客户端速度缓慢的时候,可以将这二个参数一起参考设定。该内核参数默认值一般是128,对于负载很大的服务程序来说大大的不够。一般会将它修改为2048或者更大。在/etc/sysctl.conf中添加:net.core.somaxconn = 2048,然后在终端中执行sysctl -p。
tcp-backlog 511
#指定 redis 只接收来自于该 IP 地址的请求,如果不进行设置,那么将处理所有请求
#bind 127.0.0.1
#bind 0.0.0.0
#配置unix socket来让redis支持监听本地连接。
# unixsocket /var/run/redis/redis.sock
#配置unix socket使用文件的权限
# unixsocketperm 700
# 此参数为设置客户端空闲超过timeout,服务端会断开连接,为0则服务端不会主动断开连接,不能小于0。
timeout 0
#tcp keepalive参数。如果设置不为0,就使用配置tcp的SO_KEEPALIVE值,使用keepalive有两个好处:检测挂掉的对端。降低中间设备出问题而导致网络看似连接却已经与对端端口的问题。在Linux内核中,设置了keepalive,redis会定时给对端发送ack。检测到对端关闭需要两倍的设置值。
tcp-keepalive 0
#指定了服务端日志的级别。级别包括:debug(很多信息,方便开发、测试),verbose(许多有用的信息,但是没有debug级别信息多),notice(适当的日志级别,适合生产环境,默认),warn(只有非常重要的信息)
loglevel notice
#指定了记录日志的文件。空字符串的话,日志会打印到标准输出设备。后台运行的redis标准输出是/dev/null。
logfile /var/log/redis/redis-server.log
#是否打开记录syslog功能
# syslog-enabled no
#syslog的标识符。
# syslog-ident redis
#日志的来源、设备
# syslog-facility local0
#数据库的数量,默认使用的数据库是DB 0。可以通过SELECT命令选择一个db
databases 16
#当本机为 slave 服务时,设置 master 服务的 IP 地址及端口,在 Redis 启动时,它会自动从 master 进行数据同步
slaveof <masterip> <masterport>
#当 master 服务设置了密码保护时,slave 服务连接 master 的密码
masterauth <master-password>
#设置 Redis 连接密码
requirepass
# redis是基于内存的数据库,可以通过设置该值定期写入磁盘。
# 注释掉“save”这一行配置项就可以让保存数据库功能失效
# 900秒(15分钟)内至少1个key值改变(则进行数据库保存--持久化)
# 300秒(5分钟)内至少10个key值改变(则进行数据库保存--持久化)
# 60秒(1分钟)内至少10000个key值改变(则进行数据库保存--持久化)
save 900 1
save 300 10
save 60 10000
#当RDB持久化出现错误后,是否依然进行继续进行工作,yes:不能进行工作,no:可以继续进行工作,可以通过info中的rdb_last_bgsave_status了解RDB持久化是否有错误
stop-writes-on-bgsave-error yes
#使用压缩rdb文件,rdb文件压缩使用LZF压缩算法,yes:压缩,但是需要一些cpu的消耗。no:不压缩,需要更多的磁盘空间
rdbcompression yes
#是否校验rdb文件。从rdb格式的第五个版本开始,在rdb文件的末尾会带上CRC64的校验和。这跟有利于文件的容错性,但是在保存rdb文件的时候,会有大概10%的性能损耗,所以如果你追求高性能,可以关闭该配置。
rdbchecksum yes
#rdb文件的名称
dbfilename dump.rdb
#数据目录,数据库的写入会在这个目录。rdb、aof文件也会写在这个目录 redis 启动时会把 /var/lib/redis 目录下的 dump.rdb 中的数据恢复。
dir /data
#是否在每次更新操作后进行日志记录
appendonly yes
#指定更新日志文件名
appendfilename appendonly.aof
#aof持久化策略的配置
#no表示不执行fsync,由操作系统保证数据同步到磁盘,速度最快。
#always表示每次写入都执行fsync,以保证数据同步到磁盘。
#everysec表示每秒执行一次fsync,可能会导致丢失这1s数据。
appendfsync everysec
# aof自动重写配置。当目前aof文件大小超过上一次重写的aof文件大小的百分之多少进行重写,即当aof文件增长到一定大小的时候Redis能够调用bgrewriteaof对日志文件进行重写。当前AOF文件大小是上次日志重写得到AOF文件大小的二倍(设置为100)时,自动启动新的日志重写过程。
auto-aof-rewrite-percentage 100
# 设置允许重写的最小aof文件大小,避免了达到约定百分比但尺寸仍然很小的情况还要重写
auto-aof-rewrite-min-size 64mb
# 在aof重写或者写入rdb文件的时候,会执行大量IO,此时对于everysec和always的aof模式来说,执行fsync会造成阻塞过长时间,no-appendfsync-on-rewrite字段设置为默认设置为no,是最安全的方式,不会丢失数据,但是要忍受阻塞的问题。如果对延迟要求很高的应用,这个字段可以设置为yes,,设置为yes表示rewrite期间对新写操作不fsync,暂时存在内存中,不会造成阻塞的问题(因为没有磁盘竞争),等rewrite完成后再写入,这个时候redis会丢失数据。Linux的默认fsync策略是30秒。可能丢失30秒数据。因此,如果应用系统无法忍受延迟,而可以容忍少量的数据丢失,则设置为yes。如果应用系统无法忍受数据丢失,则设置为no。
no-appendfsync-on-rewrite no
#同一时间最大客户端连接数,默认0无限制
maxclients 128
# Redis 最大内存限制 字节 推荐最大物理内存的四分之三
maxmemory 104857600
#内存淘汰策略
maxmemory-policy allkeys-lru
# 慢查询队列的长度
slowlog-max-len 1000
#慢查询阈值 执行时间超过阀值的命令会被加入慢查询命令 微秒
slowlog-log-slower-than 1000
#是否启用虚拟内存机制
vm-enabled no
#虚拟内存文件路径
vm-swap-file /tmp/redis.swap
#将所有大于 vm-max-memory 的数据存入虚拟内存 当 vm-max-memory 设置为 0 的时候,所有 value 都存在于磁盘。
vm-max-memory 0
#要根据存储的 数据大小来设定的
vm-page-size 32
#swap 文件中的 page 数量
vm-pages 134217728
#设置连接swap文件的线程数,最好不要超过机器的核数,
vm-max-threads 4
#在向客户端应答时,是否把较小的包合并为一个包发送
glueoutputbuf yes
#是否激活重置哈希
activerehashing yes
#指定在超过一定的数量或者最大的元素超过某一临界值时,采用一种特殊的哈希算法
hash-max-zipmap-entries 64
hash-max-zipmap-value 512
10.常用工具
Redis支持Java客户端有Redisson、Jedis、lettuce。
Jedis是Redis的Java实现的客户端,其API提供了比较全面的Redis命令支持。使用阻塞I/O,且其方法调用都是同步的,程序流需要等到sockets处理完I/O才能执行,不支持异步。
Redisson是一个高级的分布式协调Redis客服端,能帮助用户在分布式环境中轻松实现一些Java对象。实现了分布式和可扩展的Java数据结构,提供很多分布式相关操作服务,例如,分布式锁,分布式集合,可通过Redis支持延迟队列。
和Jedis相比,功能较为简单,不支持字符串操作,不支持排序、事务、管道、分区等Redis特性。Redisson宗旨是促进使用者对Redis的关注分离,从而让使用者能够将精力更集中地放在处理业务逻辑上。
Lettuce是一个可伸缩的线程安全的Redis客户端,用于同步,异步, 多个线程可以共享同一个RedisConnection。利用nettyNIO框架来高效地管理多个连接。 支持先进的Redis功能,如Sentinel,集群,流水线,自动重新连接和Redis数据模型。
11.Redis与Memcached的区别
Redis 支持复杂的数据结构:
Redis 相比 Memcached 来说,拥有更多的数据结构,能支持更丰富的数据操作。如果需要缓存能够支持更复杂的结构和操作, Redis 会是不错的选择。
Redis 原生支持集群模式:
在 redis3.x 版本中,便能支持 Cluster 模式,而 Memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据。
性能对比:
由于 Redis 只使用单核,而 Memcached 可以使用多核,所以平均每一个核上 Redis 在存储小数据时比 Memcached 性能更高。而在 100k 以上的数据中,Memcached 性能要高于 Redis,虽然 Redis 最近也在存储大数据的性能上进行优化,但是比起 Remcached,还是稍有逊色
参考
Redis 常见面试题(2020最新版)
《进大厂系列》系列-Redis常见面试题
瑞士军刀之bitmap,HyperLoglog和GEO
