本文摘自:《JavaGuide》
redis简介
redis数据库,与传统数据库不同,redis的数据存储在内存中,读写速度非常快。
redis可以应用的方向:缓存,分布式锁,事务,持久化,LUA脚本,LRU驱动事件,多种集群方案。
为什么要用redis
高性能:
- 假设数据存储在数据库中,从中读取其实是从硬盘上读取的,读取速度相对较慢。
- 但将其存储在缓存中,再读取的时候,直接可以从缓存中获取,直接操作内存,速度很快。
- 如果数据库中的对应数据改变之后,需要同步改变缓存中响应的数据。
高并发:
- 直接操作缓存能够承受的请求远远大于直接访问数据库,所以可以把数据库中的部分数据转移到缓存中去,这样用户的一部分请求将会直接到缓存,而不经过数据库。
为什么不用map做缓存
缓存分为本地缓存和分布式缓存。
Map:
- 是本地缓存,轻量及快速,生命周期随jvm的销毁而结束。
- 在多实例的情况下,每个实例都需要保存一份缓存,缓存不具有一致性。
redis或memcached:
- 是分布式缓存,在多实例的情况下,各实例共用一份缓存,缓存具有一致性。
- 需要保持分布式缓存服务的高可用,整个程序架构上较为复杂。
redis线程模型
https://www.javazhiyin.com/22943.html
redis内部使用文件事件处理器filter event handler
,这个文件处理器是单线程的,因此redis被称为单线程的模型。
采用IO多路复用机制同时监听多个socket,根据socket上的事件来选择对应的事件处理器进行处理。
文件处理器的四部分:
- 多个socket
- IO多路复用程序
- 文件事件分派器
- 事件处理器(连接应答处理器,命令请求处理器,命令回复处理器)
多个socket可能会并发产生不同的操作,每个操作对应不同的文件事件,但是IO多路复用程序会监听多个socket,会将socket产生的事件放入队列中排队,事件分派器每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理。
redis和memcached的区别
对比参数 | redis | Memecached |
---|---|---|
类型 | 1、支持内存 2、非关系型数据库 |
1、支持内存 2、key-value键值对的形式 3、缓存系统 |
数据存储类型 | String、List、Set Hash、SortSet(Zset) |
1、文本型 2、二进制类型 |
操作类型 | 1、批量操作 2、事务支持(假事务) 3、每个类型不同的CRUD |
1、CRUD 2、少量的其他命令 |
附加功能 | 1、发布订阅模式 2、主从分区 3、序列化支持 4、脚本支持(LUA脚本) |
多线程服务支持 |
网络IO模型 | 单线程模型 | 多线程、非阻塞IO模式 |
事件库 | 自动简易事件库AeEvent | 贵族血统的LibEvent事件库 |
持久化支持 | RDB、AOF | 不支持 |
redis常见数据结构以及使用场景分析
String
常用操作:set、get、decr、incr、mget
String数据结构是简单的key-value类型,应用于常规的缓存应用:微博数、粉丝数等。
Hash
常用操作:hget、hset、hgetall
String类型的field和value的映射表,hash适合用于存储对象,后续操作时,可以直接仅仅修改该对象中的某个字段的值。如:
key=user122
value={
"id": 1,
"name":"summerday",
"age":20
}
List
常用操作:lpush、rpush、lpop、rpop、lrange
双向链表,可以支持双向查找遍历,可以实现消息列表,粉丝关注列表的,lrange命令可以实现从某个元素开始读取多少个元素,可以实现简单的高性能分页。
Set
常用操作:sadd,spop,smembers,sunion
提供类似list的功能,但是set中元素自动去重,另外交集并集差集的操作可以轻松实现共同关注、共同粉丝的功能。
Sorted Set
常用操作:zadd、zrange、zrem、zcard
在set的基础上增加了一个权重系数score,使得集合中的元素能够按score进行有序排列,可以实现各种排行榜。
redis设置过期时间功能
一般来说,像验证码,token等登录信息都会有时间限制,按照传统数据库的处理方式,可能就需要自己去判断是否已经过期,这样会影响一部分项目性能。
而redis拥有设置过期时间的功能,在set key的时候,设置一个expire time,通过这个过期时间指定key存活时间。
redis删除key的两种方式:
-
定期删除:每隔100ms随机抽取一些设置了过期时间的key,检查他们是否过期,如果过期就删除。注:随机抽取是为了防止key数量过多而导致的cpu负载。
-
惰性删除:定期删除可能会导致许多过期的key到了时间并没有被删除掉,这时就有了惰性删除,如果我们设置的过期key到了指定的时间还没有被删除掉,他还停留在内存中,除非系统去查那个key,那个key才会被redis删除。
假如我们设置了大量过期时间的key,定期删除时漏掉了许多key,系统也没有及时去查这个key,那么将会导致一种严重的情况:大量过期的key堆积在内存中,导致redis内存耗尽。
这时就需要用到redis的内存淘汰机制。
redis提供数据淘汰策略
- volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据,淘汰之。
- volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰。
- volatile-random:从已设置过期时间的数据集中任意选择数据淘汰。
- allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。(最常用)
- allkeys-random:从数据集中任意选择数据淘汰。
- no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。
4.0版本之后新增:
- volatile-lfu:从已设置过期时间的数据集中挑选最不经常使用的数据淘汰。
- allekeys-lfu:当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的key。
redis的持久化机制
redis支持两种持久化的操作,而memcached并不支持持久化操作。
- RDB,快照:redis可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本,Redis创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(主从结构),还可以将快照留在原地以便重启服务器时使用。
快照持久化时redis默认采用的持久化方式。
-
AOF,只追加文件,append-only file:实时性更好,已成为主流的持久化方案。默认情况下redis没有开启,需要通过appendonly yes开启。
开启AOF持久化之后,每执行一条会更改redis中的数据的命令,redis就会将该命令写入硬盘中的AOF文件。
redis在配置文件中存在三种不同的AOF持久化方式:
appendfsync always # 每次有数据修改发生时,都会写入AOF文件,这样会严重降低redis的速度 appendfsync everysec # 每秒钟同步一次,显式地将多个写命令同步到磁盘 appendfsync no # 让操作系统决定合适进行同步
为了兼顾数据和写入性能,可以采用第二个。
redis事务
redis通过MULTI、EXEC、WATCH等命令来实现事务功能。
在传统的关系型数据库中,常常用ACID来检验事务功能的可靠性和安全性。
在redis中,事务总是具有原子性(Atomicity)、一致性(Consistency)和隔离性(Isolation),并且在redis运行在某种特定的持久化模式下,事务也具有持久性(Durability)。
redis缓存雪崩
缓存在同一时间大面积的失败,后面的请求都会落到数据库,造成数据库短时间内承受大量请求而崩掉。
【解决方案】
- 事前:尽量保证整个redis集群的高可用性,发现机器宕机尽快补上。选择合适的内存淘汰策略。
- 事中:本地echache缓存+hystrix限流&降级,避免Mysql崩掉。
- 事后:利用redis持久化机制保存的数据尽快恢复缓存。
缓存穿透
大量请求的key根本不存在于缓存中,导致请求直接到了数据库上,根本没有经过缓存这一层。
可以通过show variables like '%max_connections%';
命令查看MySQL的最大连接数,如果并发请求过多,数据库很容易挂掉。
【解决方案】
-
做好参数校验,一些不合法的参数请i去直接抛出异常信息返回给客户端。
-
缓存无效key:如果缓存和数据库都查不到某个key的数据,就写一个到redis中并设置过期时间,这种方式可以解决请求的key变化不频繁的情况,如果请求的key变化频繁,这种方案便不能解决这个问题。注,通常key的形式为:表名:列名:主键名:主键值
public Object getObjectInclNullById(Integer id) { // 从缓存中获取数据 Object cacheValue = cache.get(id); // 缓存为空 if (cacheValue == null) { // 从数据库中获取 Object storageValue = storage.get(key); // 缓存空对象 cache.set(key, storageValue); // 如果存储数据为空,需要设置⼀个过期时间(300秒) if (storageValue == null) { // 必须设置过期时间,否则有被攻击的⻛险 cache.expire(key, 60 * 5); } return storageValue; } return cacheValue; }
-
使用布隆过滤器:通过它判断一个给定的数据是否存在于海量数据库中,当用户请求过来,判断用户发来请求的值是否存在于布隆过滤器中,不存在的话,直接返回请求参数信息给客户端。
如何保证缓存与数据库双写时数据一致性
- 读请求和写请求串行化,串到一个内存队列中,可以保证一致性,但会导致系统的吞吐量大幅度降低。
- Cache Aside Pattern
- 读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。
- 更新的时候,先更新数据库,然后再删除缓存。