chenjunda
长期主义

redis学习Day2

2025-09-29 redis

1.为什么redis比mysql要快?

  • 内存存储:Redis 是基于内存存储的 NoSQL 数据库,而 MySQL 是基于磁盘存储的关系型数据库。由于内存存储速度快,Redis 能够更快地读取和写入数据,而无需像 MySQL 那样频繁进行磁盘 I/O 操作。
  • 简单数据结构:Redis 是基于键值对存储数据的,支持简单的数据结构(字符串、哈希、列表、集合、有序集合)。相比之下,MySQL 需要定义表结构、索引等复杂的关系型数据结构,因此在某些场景下 Redis 的数据操作更为简单高效,比如 Redis 用哈希表查询, 只需要O1 时间复杂度,而MySQL引擎的底层实现是B+Tree,时间复杂度是O(logn)
  • 线程模型:Redis 采用单线程模型可以避免了多线程之间的竞争,省去了多线程切换带来的时间和性能上的开销,而且也不会导致死锁问题。

2.本地缓存和Redis缓存的区别?

本地缓存是指将数据存储在本地应用程序或服务器上,通常用于加速数据访问和提高响应速度。本地缓存通常使用内存作为存储介质,利用内存的高速读写特性来提高数据访问速度。

本地缓存的优势:

  • 访问速度快:由于本地缓存存储在本地内存中,因此访问速度非常快,能够满足频繁访问和即时响应的需求。
  • 减轻网络压力:本地缓存能够降低对远程服务器的访问次数,从而减轻网络压力,提高系统的可用性和稳定性。
  • 低延迟:由于本地缓存位于本地设备上,因此能够提供低延迟的访问速度,适用于对实时性要求较高的应用场景。

本地缓存的不足:

  • 可扩展性有限:本地缓存的可扩展性受到硬件资源的限制,无法支持大规模的数据存储和访问。

**分布式缓存(Redis)**是指将数据存储在多个分布式节点上,通过协同工作来提供高性能的数据访问服务。分布式缓存通常使用集群方式进行部署,利用多台服务器来分担数据存储和访问的压力。

分布式缓存的优势:

  • 可扩展性强:分布式缓存的节点可以动态扩展,能够支持大规模的数据存储和访问需求。
  • 数据一致性高:通过分布式一致性协议,分布式缓存能够保证数据在多个节点之间的一致性,减少数据不一致的问题。
  • 易于维护:分布式缓存通常采用自动化管理方式,能够降低维护成本和管理的复杂性。

分布式缓存的不足:

  • 访问速度相对较慢:相对于本地缓存,分布式缓存的访问速度相对较慢,因为数据需要从多个节点进行访问和协同。
  • 网络开销大:由于分布式缓存需要通过网络进行数据传输和协同操作,因此相对于本地缓存来说,网络开销较大。

在选择使用本地缓存还是分布式缓存时,我们需要根据具体的应用场景和需求进行权衡。以下是一些考虑因素:

  • 数据大小:如果数据量较小,且对实时性要求较高,本地缓存更适合;如果数据量较大,且需要支持大规模的并发访问,分布式缓存更具优势。
  • 网络状况:如果网络状况良好且稳定,分布式缓存能够更好地发挥其优势;如果网络状况较差或不稳定,本地缓存的访问速度和稳定性可能更有优势。
  • 业务特点:对于实时性要求较

3.高并发场景,Redis单节点+MySQL单节点能有多大的并发量?

  • 如果缓存命中的话,4 核心 8g 内存的配置,redis 可以支撑 10w 的 qps
  • 如果缓存没有命中的话,4 核心 8g 内存的配置,mysql 只能支持 5000 左右的 qps

4.redis应用场景是什么?

  • 缓存: Redis最常见的用途就是作为缓存系统。通过将热门数据存储在内存中,可以极大地提高访问速度,减轻数据库负载,这对于需要快速响应时间的应用程序非常重要。
  • 排行榜: Redis的有序集合结构非常适合用于实现排行榜和排名系统,可以方便地进行数据排序和排名。
  • 分布式锁: Redis的特性可以用来实现分布式锁,确保多个进程或服务之间的数据操作的原子性和一致性。
  • 计数器 由于Redis的原子操作和高性能,它非常适合用于实现计数器和统计数据的存储,如网站访问量统计、点赞数统计等。
  • 消息队列: Redis的发布订阅功能使其成为一个轻量级的消息队列,它可以用来实现发布和订阅模式,以便实时处理消息。

5.Redis支持并发操作吗?

  • 单个 Redis 命令的原子性:Redis 的单个命令是原子性的,这意味着一个命令要么完全执行成功,要么完全不执行,确保操作的一致性。这对于并发操作非常重要。
  • 多个操作的事务:Redis 支持事务,可以将一系列的操作放在一个事务中执行,使用 MULTI、EXEC、DISCARD 和 WATCH 等命令来管理事务。这样可以确保一系列操作的原子性。

6.Redis分布式锁的实现原理?什么场景下用到分布式锁?

Redis 本身可以被多个客户端共享访问,正好就是一个共享存储系统,可以用来保存分布式锁,而且 Redis 的读写性能高,可以应对高并发的锁操作场景。Redis 的 SET 命令有个 NX 参数可以实现「key不存在才插入」,所以可以用它来实现分布式锁:

  • 如果 key 不存在,则显示插入成功,可以用来表示加锁成功;
  • 如果 key 存在,则会显示插入失败,可以用来表示加锁失败。

基于 Redis 节点实现分布式锁时,对于加锁操作,我们需要满足三个条件。

  • 加锁包括了读取锁变量、检查锁变量值和设置锁变量值三个操作,但需要以原子操作的方式完成,所以,我们使用 SET 命令带上 NX 选项来实现加锁;
  • 锁变量需要设置过期时间,以免客户端拿到锁后发生异常,导致锁一直无法释放,所以,我们在 SET 命令执行时加上 EX/PX 选项,设置其过期时间;
  • 锁变量的值需要能区分来自不同客户端的加锁操作,以免在释放锁时,出现误释放操作,所以,我们使用 SET 命令设置锁变量值时,每个客户端设置的值是一个唯一值,用于标识客户端;

满足这三个条件的分布式命令如下:

1
SET lock_key unique_value NX PX 10000
  • lock_key 就是 key 键;
  • unique_value 是客户端生成的唯一的标识,区分来自不同客户端的锁操作;
  • NX 代表只在 lock_key 不存在时,才对 lock_key 进行设置操作;
  • PX 10000 表示设置 lock_key 的过期时间为 10s,这是为了避免客户端发生异常而无法释放锁。

而解锁的过程就是将 lock_key 键删除(del lock_key),但不能乱删,要保证执行操作的客户端就是加锁的客户端。所以,解锁的时候,我们要先判断锁的 unique_value 是否为加锁客户端,是的话,才将 lock_key 键删除。

可以看到,解锁是有两个操作,这时就需要 Lua 脚本来保证解锁的原子性,因为 Redis 在执行 Lua 脚本时,可以以原子性的方式执行,保证了锁释放操作的原子性。

1
2
3
4
5
6
// 释放锁时,先比较 unique_value 是否相等,避免锁的误释放
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end

这样一来,就通过使用 SET 命令和 Lua 脚本在 Redis 单节点上完成了分布式锁的加锁和解锁。

7.库存超卖问题分析

有关超卖问题分析:在我们原有代码中是这么写的

1
2
3
4
5
6
7
8
9
10
11
12
if (voucher.getStock() < 1) {
// 库存不足
return Result.fail("库存不足!");
}
//5,扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock= stock -1")
.eq("voucher_id", voucherId).update();
if (!success) {
//扣减库存
return Result.fail("库存不足!");
}

假设线程1过来查询库存,判断出来库存大于1,正准备去扣减库存,但是还没有来得及去扣减,此时线程2过来,线程2也去查询库存,发现这个数量一定也大于1,那么这两个线程都会去扣减库存,最终多个线程相当于一起去扣减库存,此时就会出现库存的超卖问题。

悲观锁:

悲观锁可以实现对于数据的串行化执行,比如syn,和lock都是悲观锁的代表,同时,悲观锁中又可以再细分为公平锁,非公平锁,可重入锁,等等

乐观锁:

乐观锁:会有一个版本号,每次操作数据会对版本号+1,再提交回数据时,会去校验是否比之前的版本大1 ,如果大1 ,则进行操作成功,这套机制的核心逻辑在于,如果在操作过程中,版本号只比原来大1 ,那么就意味着操作过程中没有人对他进行过修改,他的操作就是安全的,如果不大1,则数据被修改过,当然乐观锁还有一些变种的处理方式比如cas

乐观锁的典型代表:就是cas,利用cas进行无锁化机制加锁,var5 是操作前读取的内存值,while中的var1+var2 是预估值,如果预估值 == 内存值,则代表中间没有被人修改过,此时就将新值去替换 内存值

乐观锁解决超卖问题

修改代码方案一、

VoucherOrderServiceImpl 在扣减库存时,改为:

1
2
3
boolean success = seckillVoucherService.update()
.setSql("stock= stock -1") //set stock = stock -1
.eq("voucher_id", voucherId).eq("stock",voucher.getStock()).update(); //where id = ? and stock = ?

以上逻辑的核心含义是:只要我扣减库存时的库存和之前我查询到的库存是一样的,就意味着没有人在中间修改过库存,那么此时就是安全的,但是以上这种方式通过测试发现会有很多失败的情况,失败的原因在于:在使用乐观锁过程中假设100个线程同时都拿到了100的库存,然后大家一起去进行扣减,但是100个人中只有1个人能扣减成功,其他的人在处理时,他们在扣减时,库存已经被修改过了,所以此时其他线程都会失败

修改代码方案二、

之前的方式要修改前后都保持一致,但是这样我们分析过,成功的概率太低,所以我们的乐观锁需要变一下,改成stock大于0 即可

1
2
3
boolean success = seckillVoucherService.update()
.setSql("stock= stock -1")
.eq("voucher_id", voucherId).update().gt("stock",0); //where id = ? and stock > 0

知识小扩展:

针对cas中的自旋压力过大,我们可以使用Longaddr这个类去解决

Java8 提供的一个对AtomicLong改进后的一个类,LongAdder

大量线程并发更新一个原子性的时候,天然的问题就是自旋,会导致并发性问题,当然这也比我们直接使用syn来的好

所以利用这么一个类,LongAdder来进行优化

如果获取某个值,则会对cell和base的值进行递增,最后返回一个完整的值

Author: chenjunda

Link: http://example.com/2025/09/29/redis%E5%AD%A6%E4%B9%A0Day2/

Copyright: All articles in this blog are licensed under CC BY-NC-SA 3.0 unless stating additionally.

< PreviousPost
560.和为K的子数组
NextPost >
diary2025-9-22
CATALOG
  1. 1. 1.为什么redis比mysql要快?
  2. 2. 2.本地缓存和Redis缓存的区别?
  3. 3. 3.高并发场景,Redis单节点+MySQL单节点能有多大的并发量?
  4. 4. 4.redis应用场景是什么?
  5. 5. 5.Redis支持并发操作吗?
  6. 6. 6.Redis分布式锁的实现原理?什么场景下用到分布式锁?
  7. 7. 7.库存超卖问题分析
    1. 7.0.1. 乐观锁解决超卖问题