Redis

Redis是一个高性能的键值存储系统,基于内存且支持持久化,提供多种数据结构如String、List、Set、Hash和Zset。其持久化包括RDB快照和AOF日志,集群方案包括主从复制、哨兵模式和分片集群。此外,文章还讨论了缓存穿透、雪崩和击穿的解决方案,以及分布式锁的实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Redis基本特性

  • 诞生于2009年,基于C语言编写,全称是Remote Dictionary Server
  • Redis作者本来只发布了linux版本,是微软团队编译了windows版本的redis
  • 单线程,每个命令具备原子性(虽然Redis说是多线程,但是核心部分还是单线程,多线程只是为了处理网络请求)
  • 虽然单线程,但是性能高的原因:基于内存,IO可以多复用
  • 支持数据持久化、主从集群、分片集群(分片集群:数据可以拆成几块存储,实现水平扩展)。
  • 关系型数据库和非关系型数据库对比:
    • 关系型数据库满足事务性ACID,非关系型数据库不满足事务性要求,或者仅满足部分事务性要求
    • 关系型数据库数据之间是关联的,非关系型数据库数据之间不是关联的。
    • 关系型数据库多储存在磁盘,非关系型数据库多存储在内存。

启动

  • redis配置文件方式启动:redis-server redis.conf
  • redis.conf内容:
    • 默认端口:port 6379
    • 数据库数量,默认有16个库,编号0-15:databases 1
    • redis能够使用的最大内存:默认512M
    • 守护进程:daemonize yes,设置为yes后redis就可以后台运行

数据结构

  • String:
  • list:链表结构,有序,元素可重复
  • set:和java中的set一样,可以保存多个字符串元素,集合中的元素不能重复,并且结合无序。
  • hash:链表+数组(红黑树)的数据结构存储
    • 根据key值进行hash算法取模,例如得到3,则在链表第三个位置插入数据。
    • 如果多个key进行hash算法取模得到的值相同,则称为hash冲突
    • 怎么解决hash冲突?利用数组,如果有冲突,元素往数组后插入。
    • 如果超过hash冲突的个数超过一定数量,则使用红黑树代替数组进行存储。
    • 关于hash数据结构,参考这个文章,讲的不错:文章地址
  • zset(sortedSet):内部使用了两种数据结构hash表和跳表(skipList)
    • 跳表:加入了几级索引
  • Geo:存储地理位置信息,并对存储的信息进行操作,例如存储地理位置坐标信息,计算两个坐标之间的距离等。
  • bitmaps:相当于位数组,每个单元只能存0和1,数组的下标在Bitmaps中叫做偏移量。
    • BitMaps使用场景:统计用户的用户在线状态,例如把用户的ID作为偏移量,如果在线就为1,不在线就为0
    • 用户签到统计,根据日期开辟一个BitMaps变量空间,用户的ID作为偏移量,如果已签到就为1,未签到就为0
  • HyperLogLog:基数统计
    • 先说什么是基数:比如数据集 {1, 3, 5, 7, 5, 7, 8}, 那么这个数据集的基数集为 {1, 3, 5 ,7, 8}, 基数(不重复元素)为5。
    • 关于HeyerLoglog的原理和应用,这篇文章讲的很不错:点击这里
  • 几种数据结构应用场景,可参考这篇文章
数据结构应用场景
String存储用户信息、点赞统计、分布式锁、缓存对象、数据统计、时间内限制请求次数、订单号(全局唯一)、分布式session
List数据字典、消息队列
Set文章关联用户点赞、浏览信息、抽奖活动、好友人脉
Zset排行榜、新闻热搜
HashRedisson分布式锁、购物车列表、缓存对象
Geo存储地理位置信息
Bitmaps统计用户是否签到、统计用户在线状态
HyperLogLog大数据量的统计,比如页面访问量统计或者用户访问量:统计统计注册 IP 数,统计每日访问 IP 数,统计页面实时 UV 数,统计在线用户数,统计用户每天搜索不同词条的个数

持久化

持久化这一部分参考了如下文章:文章1文章2
先说结论:我认为的RDB和AOF分别相当于ORCLE数据库的全量备份和增量备份。但是又有区别:

  • 在Redis4.0之前,Redis重启后恢复数据时,如果检测到有AOF存在,就不会再恢复RDB,那么AOF就不是真正意义上的增量备份,因为增量备份至少要有个最近时间点的全量备份作为数据基础
  • 在Redis4.0之后,Redis有了混合持久化的概念,这个概念就接近于关系型数据中的全量备份你和增量备份,在文章1 中最后一部分讲到了混合持久化的概念,引用下原话:
    • Redis 4.0 提供了混合持久化方案,将 RDB 文件的内容和增量的 AOF 日志文件存在一起。
    • 这里的 AOF 日志不再是全量的日志,而是自 RDB 持久化开始到持久化结束这段时间发生的增量 AOF 日志。
    • 通常这部分日志很小。

再看看RDB和AOF的具体区别吧:

  • RDB:记录快照,将数据全量保存,备份数据时性能耗费较大,但是恢复数据时会比AOF性能要好,但是可能会丢失在两次执行RDB备份时间间隔过程的数据。RDB机制触发的三种方法:
    1. save方法会阻塞主线程,大量IO操作会影响功能使用,所以在生产环境强烈不推荐使用
    2. bgsvae方法会fork出新的子线程,不会影响主线程使用
    3. redis.config配置自动化备份,save 100 1表示100秒内如果有一次或者一次以上的写入或修改操作,那么就会执行RDB备份,相同道理save 200 10表示200秒内如果有10次及以上的写入或者修改操作,那么就执行RDB备份。在redis.config中,还有几个其他的重要参数:
      • stop-writes-on-bgsave-error:默认值为yes,即当最后一次 RDB 持久化保存文件失败后,拒绝接收数据。这样做的好处是可以让用户意识到数据并没有被成功地持久化,避免后续更严重的业务问题的发生;
      • rdbcompression:默认值为yes,即代表将存储到磁盘中的快照进行压缩处理;
      • rdbchecksum:默认值为yes,在快照存储完成后,我们还可以通过CRC64算法来对数据进行校验,这会提升一定的性能消耗;
      • dbfilename:默认值为dump.rdb,即将快照存储命名为dump.rdb;
      • dir:设置快照的存储路径。
  • AOF:记录Redis的修改命令,形成日志文件,通过重放日志文件进行恢复,虽然不会数据丢失但是,效率远不如RDB。AOF记录的东西有点像MYSQL的binlog日志或者Oracle的归档日志。AOF默认不开启,AOF机制有三种配置:
    1. always:每次修改数据就会记录到文件中,虽然能保存完整的数据,但是性能很差。
    2. everysec:每一秒进行同步,速度有提升,但是也无法解决1s的数据间隙问题。
    3. no:默认配置,即不使用 AOF 持久化方案。

AOF优化

问题1:如果每一次修改数据就记录文件,是不是太耗费性能了,并且并发量高的时候,是不是容易丢失数据?并且在记录日志的过程,有可能会有写入数据的操作,那么数据就不一致了,怎么解决?

  • 针对这个问题,就引入aof_buf缓冲区进行解决,记录一段写入日志后,择机写入文件。

问题2:AOF在写入磁盘的过程中,可能会存在多条无用的数据,例如多次set某个key的值,那理论上是不是我只记录最后一次key对应的值就可以了?又或者incr自增,执行了好多次之后,我是不是也可以只记录最后一次的值?

  • 可以对aof文件进行精简,那么对于AOF文件精简的过程,就叫重写(rewrite)
  • 重写一般会fork一个子进程进行操作,但是在重写期间,主进程仍然可能出现数据不一致情况,所以就要引入aof_rewrite_buf缓冲区的概念。
  • 当fork一个子进程的同时,创建一个aof_rewrite_buf重写缓冲区,记录主进程的写入记录,等重写结束后,将主进程的写入记录,写入新的aof文件中。

插一句题外话:mysql的binlog日志和redo日志

刚刚在持久化这章节,提到mysql的binlog日志,仔细查阅资料后,我意识到自己一直把mysql的binlog日志和redo日志搞混了,既然讲到这里,那就从这篇文章中摘抄下两者区别:

  • binlog 是 MySQL 的逻辑日志,也叫归档日志、二进制日志,由 MySQL Server 来记录。用于记录用户对数据库操作的SQL语句(除了查询语句)信息,以二进制的形式保存在磁盘中。
  • redolog 是 MySQL 的物理日志,也叫重做日志,记录存储引擎 InnoDB 的事务日志,储存了数据更新后的值。
    • MySQL 每执行一条 SQL 更新语句,不是每次数据更改都立刻写到磁盘,而是先将记录写到 redo log 里面,并更新内存(这时内存与磁盘的数据不一致,将这种有差异的数据称为脏页),一段时间后,再一次性将多个操作记录写到到磁盘上,这样可以减少磁盘 io 成本,提高操作速度。
    • 先写日志,再写磁盘,这就是 MySQL 里经常说到的 WAL 技术,即 Write-Ahead Logging,又叫预写日志。MySQL 通过 WAL 技术保证事务的持久性。

集群(集群往后内容实在太多了,有点总结不过来,还是采用总结备忘+引用链接的方式进行记录吧)

有些关于redis集群方案的博客,上来就开始落落什么Redis Cluster、Redis Sharding之类的,倒也没什么毛病,但是不管是Cluster还是Sharding,都只能算是分片集群的实现方式,并不能体现Redis集群的所有模式,那么集群的模式我认为有一下几种:

  1. 主从集群(master/slave),可以演变为读写分离。
  2. 哨兵模式,基于主从集群,引入哨兵进行监控和故障处理,Redis Sentinel 的节点数量要满足 2n+1(n>=1) 的奇数个。
    • 主观下线
    • 客观下线
  3. 分片集群,根据不同的key进行hash算法,存储在不同的redis实例中,这部分不过多记录了,网上很多博客讲的都很好,推荐一个文章:Redis集群。另外,在分片集群中,根据HASH路由算法所在位置不同,大致分为三种实现方式:
    • 服务端Sharding:Redis官方集群方案 Redis Cluster,技术,3.0版本开始正式提供,采用slot槽的概念,共分为16384个槽位,对键值对进行CRC16后除以16384取模,然后放在对应槽位中。
    • 客户端Sharding:Redis Sharding
    • 代理Sharding(中间件第三方路由):twemproxy

Redis分片存储的缺点:

  • 跨实例批量操作多个key,涉及多个key并且key不在同一个实例。
  • 跨实例批量操作多个key的事务问题。
  • 当使用分区的时候,数据处理会非常复杂,例如为了备份你必须从不同的 Redis 实例和主机同时收集 RDB / AOF 文件。
  • 分区时动态扩容或缩容可能非常复杂。Redis 集群在运行时增加或者删除 Redis 节点,能做到最大程度对用户透明地数据再平衡,但其他一些客户端分区或者代理分区方法则不支持这种特性。然而,有一种预分片的技术也可以较好的解决这个问题。

节点扩容问题

关于一致性算法和HASH环,有两个比较不错的文章:文章1文章2

缓存穿透、缓存击穿、缓存雪崩

  • 缓存穿透
    • 场景一:由于多并发情况下,前几个请求可能同时执行到查询数据库的操作时,会产生短暂的缓存穿透。
    • 场景二:查询的数据在数据库中不存在,并且没有被记录在缓存中,会造成缓存穿透。
    • 解决方案一:增加空对象的缓存,即即使查询的数据在数据库中不存在,也要在缓存中存入id:null数据,但是这样操作一定要加入一个短暂的过期时间用于及时刷新缓存,否则等到这个数据在数据库中真实存在了,也会被缓存给返回个空对象。
    • 解决方案二:如果主键是整形,那么可以对请求的ID进行数据类型校验,发现不是整形的就直接return null。
    • 解决方案三:使用布隆过滤器,在数据库插入操作时,对数据进行白名单维护,数据请求的时候先在白名单中进行比对,如果不在白名单就直接return null。
  • 缓存雪崩
    • 场景一:由于设置的过期时间差不多,大量的热key同时过期,那下个请求波峰肯定会打到数据库中。
    • 场景二:由于高并发情况下,前几个请求同时执行到查询数据库的代码行,就会导致这几个请求都查询一遍数据库。
    • 解决方案一:错开过期时间,使用随机数的方式给过期时间进行增加或者减少,以使key均匀过期。
    • 解决方案二:针对场景二,在查询数据库代码块上增加同步锁,保证只有一个线程去查询数据库,然后写入缓存(进行代码块加同步锁的时候,一定要进行缓存二次校验,不然其他线程也一样会打到数据库中,代码见下方)。
    • 解决方案三:缓存预热。
  • 缓存击穿
    • 缓存击穿是某个热Key过期时,大量并发打到数据库上,缓存击穿和缓存雪崩有点类似,大量的缓存击穿造成的现象就是缓存雪崩。
public Object getObject(Long id){
	// 先从缓存中查询
	String jsonString = (String) redisTemplate.opsForValue().get("xxx:" + id);
	if(StringUtil.isNotEmpty(jsonString)){
		// 如果存在,从缓存中直接返回
		Object object = JSON.parseObject(jsonString, Object.class);
		return object;
	}else{
		// 如果不存在,加上同步锁之后从数据库中查询并放入缓存中,但是一定要进行缓存二次校验
		synchronized(this){
			jsonString = (String) redisTemplate.opsForValue().get("xxx:" + id);
			if(StringUtil.isNotEmpty(jsonString)){
				// 如果二次校验存在,从缓存中直接返回
				Object object = JSON.parseObject(jsonString, Object.class);
				return object;
			}else{
				// 如果二次校验缓存不存在,再查询数据库后放入缓存
				Object object = objectMapper.getById(id);
				redisTemplate.opsForValue().set("xxx:" + id, JSON.toJSONString(object), 300, TimeUnit.SECONDS);
			}
		}
	}
}

关于缓存穿透、击穿、雪崩的,这个文章写的很好,推荐阅读

大key和热key的问题及解决方案

参考这个文章

分布式锁

  • 在使用redisTimplate.opsForVaue().selfAbsent(key, value)时,如果不在finally中释放锁,或者不设置过期时间,那么其他线程无法获取到这个锁,所以执行set nx的时候,要指定过期时间:set resource-name anystring NX EX max-lock-time,调用的方法是:redisTemplate.opsForValue().selfAbsent(K var1, V var2, long var3, java.util.concurrent.TimeUnit var4),var3是过期时间
  • 上述解决方案中redis是怎么释放的锁?分为两种,但是每一种都有问题:
    • key到期时候自动释放
      • 锁会根据设置的过期时间自动释放,但也有问题,例如遇到耗时较长的查询,在查询结束之前锁自动释放了,就很容易造成重复查询
      • 针对这个问题,当然可以把锁的时间设置长一些,但是很容易影响查询效率,而且这个过期时间也不好评估,没办法设置个很合理的值。
    • 手动删除
      • 手动删除很容易和锁自动删除有所冲突,容易造成删除了其他线程的锁。例如,一笔查询业务A执行时间较长,还没有执行完key自动过期了,但是这时候恰巧另一个查询业务B进来新加两个一个锁,最后A执行完毕后又手动删除掉了B加锁的这个key,就相当于A删除掉了B的锁。所以要解决这个问题,要判断这个锁到底是不是自己加的锁。怎么解决呢?用LUA脚本解决原子性,但是很麻烦:
      • if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end
      • 在调用setnx命令的时候,设置key和value,每个线程的key和value都不一样,最终在释放锁的时候,根据key和value来判断是不是自己加的锁,防止误删别人家的锁。

另外推荐阅读如何防止重复提交

Redisson

  • 解决上述分布式锁使用lua脚本比较麻烦的问题:watchlog看门狗机制,给加锁线程A默认30s处理时间,如果超过30s没有执行完,那么启动一个watchdog线程监控这个线程A,并且自动续期30s,而其他线程一直处于自旋状态(循环获取锁资源)。
  • Redisson可以把JAVA中常用的数据结构搬到redis中去,比如List、Map、Queue等,然后java程序中像操作java自己的数据结构一样去操作redis
  • Redisson支持java中常用的并发工具类:CountDownLatch计数器、AtomicLong原子整数类、Semphore信号量类等

参考这篇文章
这篇文章

如何保存缓存一致性

根据需求来定:

  • 如果要保证强一致性(缓存数据更新和数据库必须同时更新),就要使用分布式锁来控制,修改数据库数据和写入缓存数据都用这同一个分布式锁。
  • 如果要实现最终一致性(缓存数据更新相比于数据库更新可以有短暂滞留),缓存要加过期时间,即使出现数据不一致,过期之后也会从数据库中查询后写入缓存。
  • 对于实时性要求很强,又要强一致性的,就不要用缓存了,直接从数据库中查询。
    先删缓存再更新数据库、先更新数据库再删除缓存、延迟双删,这三种方案在高并发下都无法保证数据库和缓存数据的强一致性。
    使用工具:
  1. 使用任务表加任务调度的方法进行同步
  2. 使用Canal基于Mysql的binlog进行同步
  3. 使用分布式锁进行控制,修改数据和缓存数据的操作要加分布式锁。

内存回收机制

  • 过期删除策略,redis的key生命到期了不会立即执行删除,而是使用以下两种方式进行删除:
    • 定时删除:每个key设置一个定时器,key过期就删除,如果key过多设置的定时器也就过多,耗费cpu,也可以设置一个定时器扫描多个key的删除。
    • 惰性删除:查询getkey的时候再去删除,好处是节省了cpu但是浪费了内存,而且如果一直不getkey就一直删除不了
  • 内存淘汰策略
    • LRU:Least Recently Used,最近最长时间未使用。
    • LFU:Least Frequently Used,最近最少频率使用。

lua脚本怎么保证原子性?

参考文章1
文章2

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值