文章最初发表于公众号:程序员周先森。本平台不定期更新,喜欢我的文章,请关注我的微信公众号。Redis是一个键值存储系统。它支持相对较多的存储值类型,包括string(字符串)、list(链表)、set(集合)、zset(sortedset——有序集合)和hash(散列类型)。这些数据类型支持push/pop、add/remove、intersection、union、difference等更丰富的操作,而且这些操作是原子的。在此基础上,redis支持多种方式的排序。为了保证效率,数据缓存在内存中。不同的是redis会周期性的将更新的数据写入磁盘或者将修改操作写入附加的记录文件,并在此基础上实现主从同步。简单的说,Redis就是一个数据库,但是与传统数据库不同的是,Redis的数据是存储在内存中的,所以存储和写入的速度都非常快,所以Redis在缓存方向应用的比较广泛。Redis也常用于分布式锁。Redis提供了多种数据类型来支持不同的业务场景。此外,Redis支持事务、持久化、LUA脚本、LRU驱动的事件和多集群解决方案。本文将从以下几个方向讲解Redis:为什么要用Redis来实现缓存?为什么使用Redis而不是map/guava做缓存Redis和Memcached的区别Redis常见数据结构及使用场景分析Redis设置过期时间Redis内存淘汰机制Redis持久化机制Redis事务缓存雪崩和缓存穿透问题解决Redis并发竞争如何解决关键问题如何保证缓存和数据库的数据一致性为什么要用Redis做缓存?第一个问题先抛出。既然选择使用Redis作为缓存,那么我们主要从“高性能”和“高并发”的角度来理解。高性能用户第一次访问数据,从数据库中读取。效率很低。用户访问数据存储在缓存中,二次读取可以直接从缓存中读取,效率更高。如果数据库中的数据发生变化,缓存会同步更新。数据。因为从数据库读取数据就是从硬盘读取数据,效率低。如果数据存放在缓存中,第二次读取是从缓存中读取,而从缓存中读取的数据直接对内存进行操作,效率非常高。高并发直接操作缓存比直接访问数据库能承受的请求要多得多,所以我们可以考虑将数据库中的部分数据转移到缓存中,这样用户的部分请求就不需要操作数据库了并提高高并发。为什么缓存用Redis而不用map/guava缓存分为本地缓存和分布式缓存。以Java为例,使用内置的map/guava实现本地缓存。主要特点是轻量、快速,生命周期随着JVM的销毁而结束。而且,缓存在多实例状态下不是唯一的。使用Redis作为缓存称为分布式缓存。在多实例状态下,数据的缓存是共享的,缓存是一致的。Redis和Memcached的区别Redis支持常见的数据类型:Redis不仅支持简单的key/value类型数据,还提供了string(字符串)、list(链表)、set(集合)、zset(sortedset——是有序的)collection)和hash(散列类型)等数据结构存储。Memcache仅支持简单数据类型String。Redis支持数据持久化。它可以将内存中的数据保存在磁盘上,重启时可以再次加载使用。Memecache将所有数据存储在内存中。集群模式:Memcached没有原生集群模式,需要依赖客户端向集群分片写入数据;但Redis目前原生支持Cluster模式。Memcached是一种多线程、非阻塞IO多路复用网络模型;Redis采用单线程多路复用IO多路复用模型。贴个对比图可能看起来更明显:Redis常用数据结构及使用场景分析一个字符串或一个数字。常规Key-Value缓存应用;常规统计:博客数、阅读数等。Hash常用命令:hget、hset、hgetall等。Hash特别适合存储对象。列出常用命令:lpush、rpush、lpop、rpop、lrange等。链表是Redis最重要的数据结构之一。RedisList是双向链表,支持反向查找和遍历,操作起来更方便,但带来了额外的内存开销。Set常用命令:sadd、spop、smembers、sunion等。其实Set和List都是列表选项,Set可以自动去重。当你需要存储一个没有重复数据的列表数据时,Set是最好的选择。您可以轻松地基于Set实现交、并、差运算。SortedSet常用命令:zadd、zrange、zrem、zcard等。与Set相比,SortedSet增加了一个权重参数Score,使得集合中的元素可以根据Score进行有序排列。Redis设置过期时间Redis可以为缓存中存储的数据设置过期时间。作为缓存数据库,这是一个非常有用的特性。之前写过一篇前后端交互的文章,说Token或者一些登录信息,尤其是短信验证码,是有时间限制的。按照传统的数据库处理方式,一般都是自己判断过期,这无疑会严重影响项目性能。而有一个很好的方案就是将验证信息存储在Redis中,设置过期时间。如果生存时间设置为30分钟,则数据会在半小时后从Redis中删除。说到删除,Redis是如何删除这些数据的:周期性删除:默认情况下,Redis每隔100ms随机选择一些设置了过期时间的Key,检查是否过期,如果过期则删除。为什么它是随机抽取而不是检查所有键?因为如果你设置了上万个key,每100毫秒检查一次所有存在的key,会对CPU造成很大的压力。Lazydeletion:周期性的删除可能会导致很多过期的Key到时候没有删除。当用户获取key时,如果设置了过期时间,redis会检查key是否过期,如果过期则删除key。但是周期删除+惰性删除的删除机制还有一个致命的问题:如果周期删除漏掉了很多过期的key,并且用户很长时间没有使用过这些过期的key,这些过期的key会累积在记忆。结果,Redis内存块被耗尽。于是就有了Redis内存淘汰机制的诞生。Redis内存淘汰机制Redis提供了6种数据淘汰策略:volatile-lru:从设置了过期时间的数据集中选择最近最少使用的数据进行淘汰。volatile-ttl:从设置了过期时间的数据集中选择并剔除即将过期的数据。volatile-random:从设置了过期时间的数据集中随机选择要淘汰的数据。allkeys-lru:当内存不足以容纳新写入的数据时,移除最近最少使用的key。allkeys-random:从数据集中随机选取数据进行淘汰。no-enviction:当内存不足以容纳新写入的数据时,新的写操作会报错。Redis持久化机制如何保证Redis宕机重启后数据能够恢复?很多时候我们需要持久化数据,即将内存中的数据写入硬盘。Redis持久化支持两种不同的持久化操作。接下来简单说一下Redis的两种持久化机制,RDB和AOF。SnapshotPersistence(RDB)RDB持久化是指在指定的时间间隔内,将内存中数据集的快照写入磁盘。实际操作过程是fork一个子进程,先将数据集写入一个临时文件,然后写入成功,然后替换之前的文件,用二进制压缩存储。RDB是Redis默认的持久化方式。相应目录下会生成dump.rdb文件,重启后加载dump.rdb文件恢复数据。优点:只有一个dump.rdb文件,方便持久化;容灾好,一个文件可以保存到安全盘;性能最大化,fork子进程完成写操作,让主进程继续处理命令,所以是最大IO(使用单独的子进程进行持久化,主进程不会进行任何IO操作,从而保证了redis的高性能);如果数据集太大,RDB的启动效率会比AOF高。缺点:数据安全性低。如果数据集很大,可能会导致整个服务器停止服务数百毫秒,甚至1秒。快照持久化是Redis默认采用的持久化方式。在redis.conf配置文件中已经配置:save9001:如果15分钟内至少有一个key发生变化,Redis会自动触发BGSAVE命令创建快照。save30010:5分钟内,如果至少有10个key发生变化,Redis会自动触发BGSAVE命令创建快照。save6010000:1分钟后,如果至少有10000个key发生变化,Redis会自动触发BGSAVE命令创建快照。AOFPersistenceAOF持久化是将服务器处理的每一次写入和删除操作以日志的形式记录下来。查询操作不会被记录下来,会以文本的形式记录下来,详细的操作记录可以在文件中看到。她的出现是为了弥补RDB的不足(数据不一致),所以它采用日志的形式来记录每一次写操作,并追加到文件中。当Redis重启后,会根据日志文件的内容从前到后执行write命令,完成数据恢复工作。与快照持久化相比,AOF持久化具有更好的实时性,因此成为主流的持久化方案。默认情况下,Redis不开启AOF持久化,可以通过设置appendonly参数开启:appendonlyyes开启AOF持久化后,每次执行改变Redis中数据的命令,Redis都会将命令写入AOF文件在硬盘中。AOF文件的保存位置与RDB文件相同,均由dir参数设置,默认文件名为appendonly.aof。Redis配置文件中存在三种不同的AOF持久化方式,分别是:appendfsyncalways:每次发生数据修改时写入AOF文件appendfsynceverysec:每秒同步一次,同步多个写入命令到硬盘appendfsyncno:让操作系统决定何时同步。用户可以使用appendfsynceverysec选项让Redis每秒同步一次AOF文件,这样Redis的性能几乎不会受到影响,即使出现宕机,用户最多也只会丢失一秒内产生的数据。当硬盘忙于执行写操作时,Redis也会优雅地放慢速度以适应硬盘的最大写入速度。优点:数据安全性更高,AOF持久化可以配置appendfsync属性通过append方式写入文件,即使中途宕机也可以使用redis-check-aof工具解决数据一致性问题。AOF机制的rewrite模式。缺点:AOF文件比RDB文件大,恢复速度慢;当数据集很大时,启动效率低于RDB。根据不同的同步策略,AOF在运行效率上往往比RDB慢。Redis4.0对持久化机制的优化Redis4.0支持RDB和AOF的混合持久化,但默认关闭。开启混合持久化后,AOF重写时,RDB的内容直接写入AOF文件的开头。AOF中的RDB部分是压缩格式,不再是AOF格式,可读性较差。Redis事务命令:MULTI、EXEC、WATCH等。事务提供了一种顺序执行多个命令的机制。并且在事务执行过程中,服务端会执行事务中的所有命令,然后处理其他客户端的命令请求。事务始终是原子的、一致的、隔离的,当Redis以特定的持久化模式运行时,事务也是持久的。Cache雪崩缓存处理过程:当收到请求时,先从缓存中取数据,取完后直接返回结果。如果无法获取,则从数据库中获取。直接返回空结果。缓存雪崩:缓存雪崩是指缓存中有大量数据直到过期时间,查询数据量巨大,导致数据库压力过大甚至宕机。解决方法:缓存数据的过期时间是随机设置的,防止大量数据同时过期。如果缓存数据库采用分布式部署,则将热点数据均匀分布在不同的缓存数据库中。设置热点数据永不过期。缓存穿透介绍:缓存穿透是指数据既不在缓存中也不在数据库中,但用户不断发起请求,比如id为“-1”的数据或者id特别大的不存在的数据.这个时候用户很可能就是攻击者,攻击会对数据库造成过大的压力。解决方案:在接口层增加校验,比如用户认证校验,id做基础校验,id<=0直接拦截;无法从缓存中获取的数据,不从数据库中获取,所以此时可以将键值对写为key-null,缓存有效时间可以设置为30秒。缓存击穿简介:缓存击穿是指数据不在缓存中,而是在数据库中。这时候由于并发量大,并没有同时读取缓存。读取数据,同时去数据库取数据,造成数据库压力瞬间增大。解决方法:设置热点数据永不过期。增加互斥体解决Redis并发争key问题问题描述:多个client同时并发写一个key,本该先到的数据可能会晚到,导致数据版本不对。或者多个client同时获取一个key,修改value再写回。只要顺序错了,数据就会错。一个key的值是1,本来按顺序改成2、3、4、最后4,但是顺序改成4、3、2,最后变成2,个人觉得分布式比较好解决locks+Timestamp:1.整体技术方案本例主要是准备一个分布式锁,大家抢锁。加锁的目的其实就是把并行读写变成串行读写的方式,避免资源竞争。用SETNX实现分布式锁非常简单。2.时间戳由于关键操作需要顺序执行,所以需要保存一个时间戳判断序列。假设系统B先抢到锁,设置key1为{ValueB7:05}。接下来系统A抢到锁,发现自己key1的时间戳早于缓存中的时间戳(7:00<=7:05),所以不执行set操作。3.什么是分布式锁?分布式锁可以有多种实现方式,比如zookeeper、redis等,无论哪种方式实现,基本原理都是一样的:用一个状态值来表示锁,通过Status来占用和释放锁值来识别。为了保证双写时缓存和数据库的数据一致性,大多数人第一个想到的方案就是将读请求和写请求序列化,串到一个内存队列中。但是这种方案有一个特别大的缺点:它还会导致系统的吞吐量显着降低,使用比正常多几倍的机器来支持线上的一个请求。最经典的缓存+数据库读写模式。读取时,先读取缓存,如果没有缓存,则读取数据库,然后取出数据放入缓存,同时返回响应。更新时,先更新数据库,再删除缓存。如果喜欢我的文章,欢迎关注我的简介公众号:程序员周先森。**
