一、Redis概念
Redis是⼀个⾼性能的key-value数据库,它是完全开源免费的,⽽且redis是⼀个NOSQL类型数据库,是为了解决⾼并发、⾼扩展,⼤数据存储等⼀系列的问题⽽产⽣的数据库解决⽅案,是⼀个⾮关系型的数据库。
二、Redis特点、数据类型及使用场景
(1)Redis基于k-v数据库(本质是一个k-v类型的内存数据库),访问速度快
(2)支持数据的持久化(可以将数据保存在硬盘,重启Redis之后可以重新写入内存)
(3)支持主从数据备份
(4)支持事务
(5)支持丰富的数据类型,主要包括string、list、hash、set、zset五种,Redis5.0以后加入了新的数据类型stream
String
常用命令:
set赋值
get获取指定key的值
mset一次为多个key赋值
mget一次获取多个key的值
setex设置key的有效时间
incr对key的值做加加操作并返回新的值
decr命令将 key 中储存的数字值减一
应用场景:String是最常用的一种数据类型,普通的key/value都可归为此类。
实现方式:String在redis内部存储默认的就是字符串,被redisObject所引用,当遇到incr、decr命令是会转成数值型进行计算,此时redisObject的encoding字段是int。
Hash
常用命令:
hset、hget、
hgetall获取一个key下面的所有filed和value
hkeys获取所有的key
hvals获取某个key下的所有value
hexists测试给定key下的filed是否存在
应用场景:存储一个用户信息对象数据,其中包括用户ID、用户姓名、年龄和生日,通过用户ID我们希望获取该用户的姓名或年龄或生日。
实现方式:
Redis的Hash实际是内部存储的value是一个HashMap,并提供了直接存取这个Map成员的接口。(key是用户ID,value是Map)
这个Map的key是成员的属性名,value是属性值,这样对数据的修改和存取都可以直接通过其内部Map的Key(Redis里称 内部Map的key为filed),也就是通过key用户id+filed属性标签即可操作对应属性数据。
当前HashMap的实现有两种⽅式:当HashMap的成员⽐较少时Redis为了节省内存会采⽤类似⼀维数组的⽅式来紧凑存储,而不会采用真正的HashMap结构,这时对应的value的redisObject的encoding为zipmap,当成员数量增大时会自动转成真正的HashMap,此时RedisObject的encoding字段为int。
List
常用命令:
lpush: 从头(链表左侧)添加元素;
lpop:从list头部获取元素
rpush:从尾部(链表右侧)添加元素;
rpop:从list尾部获取元素
lrange:查看list的所有元素;lrange list名称0 -1
linsert: 在某个元素的前后插入元素linsert list before/after原有元素新元素
lrem:移除元素Irem list 2(移除个数) "key"
rpoplpush: 从原来的list的尾部删除元素,并将其插入到新的Iist的头部
lindex: 返回指定索引的值
llen:返回list的元素 个数
应用场景:Redis list的应用场景非常多,也是Redis最重要的数据结构之一,比如twitter的关注列表,粉丝列表等都可以用Redis的list结构来实现;
实现方式:Redis list的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销,Redis内部的很多实现,包括发送缓冲队列等也都是用的这个数据结构。
Set
常用命令:
sadd:添加元素
smembers:获取集合中所有元素
sismember:判断元素是否在集合中
srem:删除元素
scard:获取元素个数,相当于count
spop:随机返回删除的元素
sdif:差集,返回在第-个set里面而不在后面任何一个set里面的项(谁在前以谁为标准)
sdiffstore:差集并保留结果
sinter:交集,返回多个set里面都有的项
sinterstore:交集并保留结果
sunion:并集
sunionstore:并集并保留结果
smove:移动元素到另一个集合
应用场景:Redis set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供
的;
实现方式:set的内部实现是一个value永远为null的HashMap,实际就是通过计算hash的方式来快速排重的,这也是set能提供判断一个成员是否在集合内的原因。
Sorted Set
常用命令:
zadd:添加元素
zrange:获取索引区间内的元素
zrangebyscore:获取分数区间内的元素
应用场景:Redis sorted set的使用场景与set类似,区别是set不是自动有序的,而sorted set可以通过用户额外提供一个优先级(score)的参数来为成员排序,并且是插入有序的,即自动排序。当你需要一个有序的并且不重复的集合列表,那么可以选择sorted set数据结构.比如twitter 的public timeline可以以发表时间作为score来存储,这样获取时就是自动按时间排好序的。
实现方式:Redis sorted set的内部使用HashMap和跳跃表(SkipList)来保证数据的存储和有序,HashMap里放的是成员到score的映射,而跳跃表里存放的是所有的成员,排序依据是HashMap里存的score,使用跳跃表的结构可以获得比较高的查找效率,并且在实现上比较简单。
三、Redis和MemCache区别
1.在存储方式方面
Memecache把数据全部存在内存中,不能持久化数据;
Redis支持数据的持久化(RDB和AOF)
RDB:在不同的时间点,将Redis某一时刻的数据生成 快照,并保存在磁盘上。
触发方式:自动触发(在redis.conf文件配置)、手动触发(save命令:执行此命令会阻塞Redis服务器;bgsave命令:执行该命令时,Redis会在后台异步进行快照操作)。
AOF: 只允许追加不允许改写文件,是将 Redis执行过的所有写指令记录下来,在下次redis重启的时候,只需要把这些指令从前到后重复执行一遍,就可以实现数据的恢复。
默认的AOF持久化策略是每秒钟一次同步策略(AOF同步策略有always、no、everysec三种)。
RDB和AOF两种方式可以同时使用,这时如果Redis重启则会优先采用AOF方式进行数据恢复,因为AOF方式的数据恢复完整度更高。
2.在数据类型方面
Memcache所有的值都是简单的字符串;
Redis支持更丰富的数据类型。
3.在底层模型方面
redis在2.0版本后增加了自己的VM特性,突破物理内存的限制;
Memcache可以修改:最大可用内存,采用L RU算法。
4.数据一致性方面
Memcache在并发场景下,用CAS保证一致性;
Redis事务支持比较弱,只能保证事务中的每个操作连续执行。
Memcache的介绍
1. Memcached的优点:
Memcached可以利用多核优势,单实例吞吐量极高,可以达到几十万QPS (取决于key、 value的字节大小以及服务器硬件性能,日常环境中QPS高峰大约在46w左右)。
2. Memcached的局限性:
只支持简单的key/value数据结构,不像Redis可以支持丰富的数据类型。
无法进行持久化,数据不能备份,只能用于缓存使用,且重启后数据全部丢失。
无法进行数据同步,不能将Memcached中的数 据迁移到其他Memcached实例中。
Memcached内存分配采用Slab Allocation机制管理内存,value大小分布 差异较大时会造成内存利用率降低,并引发低利用率时依然出现踢出等问题。
需要用户注重value设计。
四、关于Redis的回收策略,过期机制的相关问题
Redis内存数据集上升到一定大小的时候,会实行数据淘汰策略。
Redis key过期的方式有三种:
1.被动删除: 当读、写一个已经过期的key时,会触发惰性删除策略,直接删除掉过期的key
2.主动删除:由于惰性删除策略无法保证冷数据被及时删掉,所以Redis会定期主动淘汰一批已过期的key
3.当前已用内存超过maxmemory时,触发主动清理策略
当前已用内存超过maxmemory限定时,触发主动清理策略,主动清理策略有6种:
1.vltil-lru:从已设置过期时间的数据集中挑选"最近最少使用"的数据进行淘汰;
2.volitilet:从已设置过期时间的数据集中挑选“将要过期”的数据进行淘汰;
3. volitile random:从已设置过期时间的数据集中"任意挑选数据进行淘汰;
4. alkeys-lru:从数据集中挑选“最近最少使用”的数据进行淘汰
5. allkeys-random:从数据集中"任意选择"数据进行淘汰;
6. no envicition:设置永不过期,禁止驱逐数据;
五、为什么redis需要把所有数据放到内存中?
Redis为了达到最快的读写速度将数据都读到内存中,并通过异步的方式将数据写入磁盘。所以redis具有快速和数据持久化的特征。如果不将数据放在内存中,磁盘I/O速度为严重影响redis的性能。在内存越来越便宜的今天,redis将会越来越受欢迎。
如果设置了最大使用的内存,则数据已有记录数达到内存限值后不能继续插入新值。
六、Redis是单进程单线程的
Redis利用队列技术将并发访问变为串行访问,消除了传统数据库串行控制的开销。
七、Redis的并发竞争问题如何解决?
Redis为单进程单线程模式,采用队列模式将并发访问变为串行访问。Redis本身没有锁的概念,Redis对于多个客户端连接并不存在竞争,但是在Jedis客户端对Redis进行并发访问时,会发生连接超时、数据转换错误、阻塞、客户端关闭连接等问题,这些问题均是由于客户端连接混乱造成。
对此有2种解决方法:
(1)客户端角度,为保证每个客户端间正常有序与Redis进行通信,对连接进行池化,同时对客户端读写Redis操作采用内部锁synchronized。
(2)服务器角度,利用setnx实现锁。
注:对于第一种,需要应用程序自己处理资源的同步,可以使用的方法比较通俗,可以使用synchronized也可以使用lock;第二种需要用到Redis的setnx命令,但是需要注意一些问题。
八、Redis缓存的三大问题及其解决方案
8.1、缓存穿透
缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求。如发起 id 为-1 的数据或者特别大的不存在的数据。有可能是黑客利用漏洞攻击从而去压垮应用的数据库。
解决方案:
(1)如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果(null)进行缓存,设置空结果的过期时间会很短,最长不超过五分钟,这样可以防止攻击用户反复用同一个id暴力攻击。(简单粗暴)
(2)设置可访问的名单(白名单):使用bitmaps类型定义一个可以访问的名单,名单id作为bitmaps的偏移量,每次访问和bitmap里面的id进行比较,如果访问id不在bitmaps里面,进行拦截,不允许访问。
(3)采引入布隆过滤器,在访问Redis之前判断数据是否存在。将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。(布隆过滤器是一种比较独特数据结构,有一定的误差。当它指定一个数据存在时,它不一定存在,但是当它指定一个数据不存在时,那么它一定是不存在的。)
(4)进行实时的数据监控,发现Redis在命中率急速降低时,排查访问对象和访问数据,设置黑名单。
缓存空数据与布隆过滤器都能有效解决缓存穿透问题,但使用场景有着些许不同;
当一些恶意攻击查询查询的 key 各不相同,而且数量巨多,此时缓存空数据不是一个好的解决方案。因为它需要存储所有的 Key,内存空间占用高。并且在这种情况下,很多 key 可能只用一次,所以存储下来没有意义。所以对于这种情况而言,使用布隆过滤器是个不错的选择;
而对与空数据的 Key 数量有限、Key 重复请求效率较高的场景而言,可以选择缓存空数据的方案。
Copypublic Student getStudentsByID(Long id) {
// 从Redis中获取学生信息Studentstudent= redisTemplate.opsForValue().get(String.valueOf(id));
if (student != null) {
return student;
}
// 从数据库查询学生信息,并存入Redis
student = studentDao.selectByStudentId(id);
if (student != null) {
redisTemplate.opsForValue().set(String.valueOf(id), student, 60, TimeUnit.MINUTES);
} else {
// 即使不存在,也将其存入缓存中
redisTemplate.opsForValue().set(String.valueOf(id), null, 60, TimeUnit.SECONDS);
}
return student;
}
8.2、缓存击穿
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),问题在于:如果这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。
解决方案:
(1)将热点数据设置为永不过期。这时要注意在value当中包含一个逻辑上的过期时间,然后另起一个线程,定期重建这些缓存。
(2)加载DB的时候,要防止并发。加互斥锁:互斥锁可以控制查询数据库的线程访问,但这种方案会导致系统的吞吐量下降,需要根据实际情况使用。
Copypublic String get(key) {
Stringvalue= redis.get(key);
if (value == null) {
// 代表缓存值过期 // 设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能
load db if(redis.setnx(key_mutex, 1, 3 * 60) == 1) {
// 代表设置成功
value = db.get(key);
redis.set(key, value, expire_secs);
redis.del(key_mutex);
} else {
// 这个时候代表同时候的其他线程已经load db并回设到缓存了,这时候重试获取缓存值即可
sleep(50);
get(key);
// 重试
}
} else {
return value;
}
}
8.3、缓存雪崩
缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
解决方案:
缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
如果缓存数据库是分布式部署,将热点数据均匀分布在不同搞得缓存数据库中。
设置热点数据永远不过期。
【事前】高可用缓存:高可用缓存是防止出现整个缓存故障。即使个别节点,机器甚至机房都关闭,系统仍然可以提供服务,Redis 哨兵(Sentinel) 和 Redis 集群(Cluster) 都可以做到高可用;
【事中】缓存降级(临时支持):当访问次数急剧增加导致服务出现问题时,我们如何确保服务仍然可用。在国内使用比较多的是 Hystrix,它通过熔断、降级、限流三个手段来降低雪崩发生后的损失。只要确保数据库不死,系统总可以响应请求,每年的春节 12306 我们不都是这么过来的吗?只要还可以响应起码还有抢到票的机会;
【事后】Redis 备份和快速预热:Redis 数据备份和恢复、快速缓存预热。
九、Redis的持久化机制
Redis提供了两种不同形式的持久化方式:
RDB(Redis DataBase)
AOF(Append Only File)
RDB(Redis DataBase)
Redis默认的持久化方式。按照一定的时间将内存中的数据以快照的形式保存到硬盘中,对应产生的数据文件为dump.rdb。通过配置文件中的save参数来定义快照的周期。
恢复的时候将快照文件直接读到内存里。
AOF(Append Only File)
将Redis执行的每次写命令记录到单独的日志文件中,只许追加文件但不可以改写文件,当重启Redis会重新将持久化的日志中文件恢复数据。
当两种方式同时开启的时候,数据恢复Redis会优先选择AOF恢复。
Rewrite
AOF采用文件追加方式,文件会越来越大,为避免出现此种情况,新增了重新机制,当AOF文件的大小超过所设定的阈值时,Redis就会启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集,可以使用命令bgrewriteaof。
十、如何保证Redis与数据库的数据一致?
当我们对数据进行修改的时候,到底是先删缓存,还是先写数据库?
1、如果先删缓存,再写数据库: 在高并发场景下,当第一个线程删除了缓存,还没有来得及写数据库,第二个线程来读取数据,会发现缓存中的数据为空,那就会去读数据库中的数据(旧值,脏数据),读完之后,把读到的结果写入缓存(此时,第一个线程已经将新的值写到缓存里面了),这样缓存中的值就会被覆盖为修改前的脏数据。
总结:在这种方式下,通常要求写操作不会太频繁。
解决方案:
1》先操作缓存,但是不删除缓存。将缓存修改为一个特殊值(-999)。客户端读缓存时,发现是默认值,就休眠一小会,再去查一次Redis。 -》 特殊值对业务有侵入。 休眠时间,可能会多次重复,对性能有影响。
2》延时双删。 先删除缓存,然后再写数据库,休眠一小会,再次删除缓存。-》 如果数据写操作很频繁,同样还是会有脏数据的问题。
2、先写数据库,再删缓存: 如果数据库写完了之后,缓存删除失败,数据就会不一致。
总结: 始终只能保证一定时间内的最终一致性。
解决方案:
1》给缓存设置一个过期时间 问题:过期时间内,缓存数据不会更新。
2》引入MQ,保证原子操作。
解决方案:将热点数据缓存设置为永不过期,但是在value当中写入一个逻辑上的过期时间,另外起一个后台线程,扫描这些key,对于已逻辑上过期的缓存,进行删除。
十一、如何设计一个分布式锁?如何对锁性能进行优化?
分布式锁的本质:就是在所有进程都能访问到的一个地方,设置一个锁资源,让这些进程都来竞争锁资源。数据库、zookeeper, Redis。。通常对于分布式锁,会要求响应快、性能高、与业务无关。
Redis实现分布式锁:SETNX key value:当key不存在时,就将key设置为value,并返回1。如果key存在,就返回0。EXPIRE key locktime: 设置key的有效时长。 DEL key: 删除。 GETSET key value: 先GET,再SET,先返回key对应的值,如果没有就返回空。然后再将key设置成value。
1、最简单的分布式锁: SETNX 加锁, DEL解锁。问题: 如果获取到锁的进程执行失败,他就永远不会主动解锁,那这个锁就被锁死了。
2、给锁设置过期时长: 问题: SETNX 和EXPIRE并不是原子性的,所以获取到锁的进程有可能还没有执行EXPIRE指令,就挂了,这时锁还是会被锁死。
3、将锁的内容设置为过期时间(客户端时间+过期时长),SETNX获取锁失败时,拿这个时间跟当前时间比对,如果是过期的锁,就先删除锁,再重新上锁。 问题: 在高并发场景下,会产生多个进程同时拿到锁的情况。
4、setNX失败后,获取锁上的时间戳,然后用getset,将自己的过期时间更新上去,并获取旧值。如果这个旧值,跟之前获得的时间戳是不一致的,就表示这个锁已经被其他进程占用了,自己就要放弃竞争锁。
public boolean tryLock(RedisnConnection conn){
long nowTime= System.currnetTimeMillis();
long expireTIme = nowTime+1000;
if(conn.SETNX("mykey",expireTIme)==1){
conn.EXPIRE("mykey",1000);
return true;
}else{
long oldVal = conn.get("mykey");
if(oldVal != null && oldVal < nowTime){
long currentVal = conn.GETSET("mykey",expireTime);
if(oldVal == curentVal){
conn.EXPIRE("mykey",1000);
return true;
}
return false;
}
return false;
}
}
DEL
5、上面就形成了一个比较高效的分布式锁。分析一下,上面各种优化的根本问题在于SETNX和EXPIRE两个指令无法保证原子性。Redis2.6提供了直接执行lua脚本的方式,通过Lua脚本来保证原子性。redission。
十二、Redis如何配置Key的过期时间?它的实现原理是什么?
redis设置key的过期时间:
1、 EXPIRE 。 2 SETEX
实现原理:
1、定期删除: 每隔一段时间,执行一次删除过期key的操作。
2、懒汉式删除: 当使用get、getset等指令去获取数据时,判断key是否过期。过期后,就先把key删除,再执行后面的操作。
Redis是将两种方式结合来使用。
懒汉式删除
定期删除:平衡执行频率和执行时长。
定期删除时会遍历每个database(默认16个),检查当前库中指定个数的key(默认是20个)。随机抽查这些key,如果有过期的,就删除。
程序中有一个全局变量记录到秒到了哪个数据库。
十三、海量数据下,如何快速查找一条记录?
1、使用布隆过滤器,快速过滤不存在的记录。
使用Redis的bitmap结构来实现布隆过滤器。
2、在Redis中建立数据缓存。 - 将我们对Redis使用场景的理解尽量表达出来。
以普通字符串的形式来存储,(userId -> user.json)。 以一个hash来存储一条记录 (userId key-> username field-> , userAge->)。 以一个整的hash来存储所有的数据,UserInfo-> field就用userId , value就用user.json。一个hash最多能支持2^32-1(40多个亿)个键值对。
缓存击穿:对不存在的数据也建立key。这些key都是经过布隆过滤器过滤的,所以一般不会太多。
缓存过期:将热点数据设置成永不过期,定期重建缓存。 使用分布式锁重建缓存。
3、查询优化。
按槽位分配数据,
自己实现槽位计算,找到记录应该分配在哪台机器上,然后直接去目标机器上找。