Redis 简介
Redis 是由 ANSI C(标准C语言)写的键值对存储系统,由于是存储在内存中的,所以性能极高,但我们知道存储在内存中一旦电脑关闭就会丢失数据,Redis 还提供了数据持久化的功能,也就是说,Redis 的数据也可以保存在本地硬盘中。
原子操作
所谓原子操作指的是不可再分割的操作,要么都成功,要么都失败。
Redis 的操作结果一定是确定的,例如将商品库存存储在 Redis 中,此时由于高并发场景有 1000 个请求进入,判断减少库存,总库存为 999 个,那么第 1000 个请求会因为高并发的场景而判断失误吗?答案是不会。
同理,一些抽奖活动将奖品数量存储在 Redis 进行操作,不会出现奖品被人领光还能领到奖品的情况,当有面试官问你就这么告诉他。
以抽奖场景示例:
1 | // 读取数据库奖品余额 |
在这个场景中,涉及到读后写的问题,在读取奖品余额后更新奖品数量,由于读写操作是分开的,可能因为请求的先后而产生不同结果;比如 A 进来领取奖品,奖品的库存是 1,此时 B 也进来领取奖品,这个时候他也判断了奖品库存是 1,这个时候奖品的数量只剩 1 个,却有两个人被判断为可以领取奖品,就出现奖品库存变为负数的情况。
导致问题的根本原因在于读操作与写操作是分离的,中间如果有其他的请求进来,那么读操作的结果就是不准确的,可以使用 Redis 的 decr
(自减)来操作商品库存:
1 | $redis = new Redis(); |
Redis 的 decr
命令将值减 1,并且返回了计算后的结果,实现了读写同步的操作,因此不会产生因为高并发场景读取了错误数据的情况。
这些都是依赖 Redis 的原子操作,之所以能够实现原子操作,是因为 Redis 是单线程。
假如对方问你,为什么单线程就是原子操作?
扩展阅读:什么是线程
操作系统调度任务以线程为基本单位,Redis 所有的指令都在一个线程中进行,不会扩展出额外的线程;多线程,比如 MySQL 启动的时候是一个进程,而执行增删改查操作就是一个新的线程。多线程指的是可以同时干很多件事,而单线程只能一件事一件事的干,做完一件事才做下一件事,从效率上来说多线程远远超出单线程,但单线程的优势是每一步的执行结果都是确定的,要么都成功,要么都失败。
实际上,Redis 虽然是单线程但效率也是非常高的,因为它完全基于内存操作。
键
Redis 使用键值对的形式存储数据,键类似于变量名字,键是一个字符串,最大长度 512MB,与开发语言不同的是 Redis 中的键可以使用一些开发语言中不能作为命名的符合,如:冒号、横杠。
键名称太长,会占用更多的内存空间,此外,太长的键名也会多查找造成影响;名称太短,又会造成语义不清,所以在取键名的时候需要有规范。
键的名称区分大小写,不建议大小写混用,要么全部大写,要么全部小写,取名“见名知意”为佳。
可以参考如下规则:
第一段为项目名称或者缩写形式(非必需),如:project 或 pro
第二段为表的名字,如:user
第三段为区分键的字段,如:MySQL 主键的列名,属性名
第四段为键的特征字段,可以作为查询依据,如:MySQL 中主键 ID 的值
键的各个字段通常用冒号隔开。
示例:
1 | # 根据 ID |
前缀增加项目名称的用途是:当一个 Redis 服务器部署了多个项目可以进行区分,如果是单个项目,则可以去掉项目名称。
1 | users:id:1 |
在项目中,最好有一个统一管理键名称的地方,否则后期无法进行维护。
数据类型
Redis 支持如下数据结构:
- String:字符串
- Hash:哈希
- List:列表
- Set:集合
- Sorted Set:有序集合
PHP 使用 Redis
PHP 想要使用 Redis 需要安装 redis 相关的扩展:PECL - REDIS 扩展下载。
也可以不使用扩展,直接下载 predis:Github - Predis 下载
将下载的包使用 require
命令引入:
1 | require "./lib/predis-1.1/autoload.php"; |
Redis 应用场景
缓存
由于其高性能的特性可以作为数据缓存,对于频繁查询但是不经常更新的数据可以将其缓存到 Redis 中,从而减少数据库查询压力,例如商品的库存、金额,需要注意的是当这些数据更新的时候也必须同步更新缓存数据。
此外,对于像微博的阅读量此类频繁更新的数据,也可以用缓存处理。方法是获取一条微博详情的时候,先判断是否有缓存,有的话先从缓存读取阅读量的数据,没有的话就从数据库读取,然后保存在 Redis 中;当用户访问时,直接在 Redis 操作访问量的增加,然后设置一个定时器程序,定期将缓存中的阅读量写入数据库,不过,这种类型的数据一般是比较不重要的,因为有可能会发生意外服务器重启导致数据丢失而没有正常写入数据库。
当某种数据需要频繁的查询或更新时可以使用 Redis 作为缓存。
排行榜
我们经常会收到亲朋好友要求帮忙给他们家的小盆友投票(一般是才艺表演之类的),根据点赞数进行排行,排在前几名的会有奖励之类的。排行榜的数据变化十分频繁,这个时候就可以使用 Redis 的有序集合结构来存储排行数据。
分布式锁、本地锁
Redis 中有一种命令 setnx
意为 set if not exists
,即当不存在时设置数据,否则不进行操作,利用这种特性可以实现分布式锁。当成功设置的时候就执行后面的逻辑,如果未能成功设置代表之前已经执行过了,就不再走后面的逻辑。
锁的作用是防止重复动作,比如用户在点击领取奖励的时候,发现没有响应(网络延迟导致),结果用户以为自己没点下去就多点了几次,如果不做防止重复提交的处理,很有可能会造成领取到多次奖励的 BUG。
分布式就是多台机子间,而本地锁指的是本机,其原理类似,比如设置一个带有过期时间的 String 类型的缓存,如果这个键不为空则代表加过锁了。
消息队列
Redis 中的列表结构由于读取头尾的速度非常快,因此适合作为消息队列的容器,将任务队列存储在 Redis 中可以大大提高程序的执行效率(与传统的数据库存储相比)。
Redis 安全隐患
其中一个是未设置密码问题:Redis 未设置密码导致服务器被安装挖矿病毒
此外,Redis 即使正常使用也可能存在安全隐患。
缓存分为过期缓存和不过期缓存,不过期缓存过多可能造成内存溢出,而过期缓存又可能带来新的问题。
缓存穿透
缓存穿透指的是查询一个缓存中不存在的数据,比如我们设置了一个根据用户 ID 来获取用户信息的缓存,此时如果我们输入 -1 或者其他不存在的 ID,那么系统将会判断缓存不存在,接着就去数据库查询。
这种“绕过”缓存查询数据库的行为类似于直接穿透了 Redis。
如果有人利用了这一点编写程序大量查询不存在的用户 ID 就很可能造成数据库崩溃。
解决方法是即使不存在的用户 ID 也设置一个空的缓存的键值,不过此类键值需要设置一个较短的过期时间,否则也可能会被恶意查询造成内存溢出。
缓存雪崩
雪崩指的是一种短期内产生的爆发性冲击,在 Redis 中,如果有许多缓存在同一时刻过期,就会造成大量的数据需要从数据库查询,面对冲击性涌入的查询,数据库很可能造成崩溃。
解决方法是尽可能的让缓存的过期时间不一样,热门数据的缓存时间更长一些,对于同类型的数据可以设置一个额外的随机时间来让同类型的数据也会在不同时刻过期。
除此之外,还可以设计多级缓存结构来防止缓存雪崩。
当第一层的缓存过期了,不从数据库读取,而是判断第二层缓存是否存在,如果有就直接取数据,如果没有再判断下一层缓存……以此类推。这样的结构称为多级缓存,多级缓存结构比较复杂,其中第一层缓存叫做一级缓存,第二层叫做二级缓存……多级缓存结构可以防止突发性的冲击造成数据库崩溃,不过这样系统的复杂性会变高,多级缓存需要有一个程序来定期维护下级缓存,一般由架构师进行设计。
缓存击穿
缓存击穿与缓存雪崩有些类似,不过不同的是缓存击穿指的是某个单一的键值过期,但是此时却有大量的流量涌入,造成所有的请求都直接到数据库那边,相当于在某个点凿出了一个洞,因此叫做“击穿”。
防止缓存击穿的方法:
方法(一):后台增加一个定期任务刷新缓存的过期时间。
方法(二):将缓存的过期时间也保存在值里面,当获取这个缓存数据的时候判断过期时间,在超过设置的阀值时更新这个缓存的过期时间。
方法(三):可以使用缓存雪崩所说的多级缓存方法。
方法(四):增加锁机制,当缓存过期查询数据库时,判断锁,只允许一个请求进来,其他请求都进入等待状态,进入的请求在查好数据后更新缓存,此时等待中的请求就可以获取到数据了。
总结
Redis 还有许多应用,持续保持学习中……