1.超卖问题怎么解决(redission分布式锁怎么用)
超卖问题通常出现在高并发环境下,当多个请求同时尝试对同一资源进行操作(例如购买商品库存不足的情况下仍然允许订单创建),导致该资源被多次分配或售卖。在分布式系统中,这个问题更加复杂,因为不同的服务实例可能会同时处理相同的请求。
解决超卖问题的方法
-
使用数据库事务:确保所有涉及库存减少的操作都在一个事务中完成,并且这个事务是隔离的(如使用串行化隔离级别)。但这可能会降低系统的性能和吞吐量。
-
乐观锁:在更新库存时检查版本号或者库存数量是否符合预期,如果不符合则认为操作失败并回滚。
-
悲观锁:在处理请求前锁定库存记录,直到交易完成才释放锁。这可以防止其他请求修改相同的库存数据,但可能导致死锁或活锁。
-
分布式锁:使用像 Redisson 这样的库提供的分布式锁来同步跨不同服务实例的访问。这是解决分布式环境下的超卖问题的有效方法之一。
使用 Redisson 实现分布式锁
Redisson 是一个用于 Redis 的 Java 客户端,它提供了一系列的分布式对象和服务,其中包括了分布式锁。使用 Redisson 实现分布式锁可以有效地解决在分布式系统中多个实例对共享资源进行并发访问的问题。
Redisson 分布式锁的主要特性
- 公平锁(Fair Lock):确保请求锁的顺序被执行,即先请求的线程会先获得锁。
- 可重入锁(Reentrant Lock):支持同一个线程多次获取同一把锁,并且需要等次数的解锁操作才能真正释放锁。
- 联锁(RedLock):实现跨多个 Redis 实例的一致性锁定机制,提高系统的容错能力。
- 读写锁(ReadWriteLock):允许多个线程同时读取数据,但只有一个线程可以写入数据。
- 信号量(Semaphore)和可过期信号量(PermitExpirableSemaphore):控制同时访问某个资源的最大数量,后者允许设置许可的有效期限。
- 闭锁(CountDownLatch):使一个或多个线程等待直到其他线程完成一组操作后才继续执行。
- 锁自动续期:如果锁的持有时间超过了设定的时间,Redisson 会自动尝试延长锁的有效期,以防止因长时间处理而导致的意外解锁。
以下是使用 Redisson 来避免超卖问题的基本步骤:
1. 添加依赖
首先,在你的项目中添加 Redisson 的依赖。如果你使用的是 Maven 构建工具,可以在 pom.xml 文件中添加如下依赖:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.17.6</version> <!-- 请根据需要选择最新版本 -->
</dependency>
2. 配置 Redisson 客户端
创建一个 RedissonClient 实例,连接到你的 Redis 服务器:
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redisson = Redisson.create(config);
3. 获取分布式锁
在你想要保护的代码段周围使用 Redisson 提供的 RLock 接口获取锁。这里以商品库存扣减为例:
RLock lock = redisson.getLock("lock:product:" + productId);
try {
// 尝试加锁,等待时间为10秒,锁的持有时间为30秒
boolean isLocked = lock.tryLock(10, 30, TimeUnit.SECONDS);
if (isLocked) {
// 成功获得锁后执行关键业务逻辑,比如扣减库存
// 模拟业务逻辑处理时间
Thread.sleep(1000);
// 更新数据库中的库存信息...
} else {
// 如果未能成功获取锁,则直接返回错误给用户
throw new RuntimeException("Failed to acquire lock for product " + productId);
}
} finally {
// 确保在任何情况下都能解锁,即使发生异常
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
这段代码展示了如何在执行库存扣减之前先尝试获取分布式锁。只有成功获取了锁之后才会继续执行接下来的业务逻辑。这样就可以确保同一时间内只有一个线程能够操作特定的商品库存,从而避免了超卖的问题。
锁的自动续期
对于长时间运行的任务,可以通过配置锁的自动续期功能来防止锁提前过期。Redisson 会在锁快要过期时自动延长其有效期,前提是任务仍在执行中。这项功能可以通过设置锁的租约时间(Lease Time)和使用 RLock.lock() 方法时指定合理的超时时间来实现。
注意事项:
- 尽量缩短锁的持有时间:减少锁的竞争,提高系统性能。
- 合理设置锁的等待时间和持有时间:避免长时间持有锁导致其他线程长时间等待,同时也防止锁因为网络延迟等原因而过早释放。
- 考虑使用公平锁:如果业务场景要求请求的先后顺序得到保证。
- 监控和报警:为分布式锁设置适当的监控和报警机制,以便及时发现潜在问题。
2.Redis数据持久化
Redis 提供了多种数据持久化的方式,以确保在服务器重启或崩溃后能够恢复数据。主要的持久化方式有两种:RDB(Redis Database Backup)快照和 AOF(Append Only File)日志。此外,从 Redis 4.0 开始,还引入了一种混合持久化模式。
RDB 持久化
RDB 是一种快照式的持久化方式,它会在指定的时间间隔内将内存中的数据集快照写入磁盘。这种方式的优点是性能高效,因为它是通过 fork 子进程来完成的,不会阻塞主进程处理请求。缺点是在最后一次成功保存之后新增的数据会丢失,因此不适合对数据完整性和一致性要求非常高的场景。
触发条件:
- 配置文件中默认的快照配置
- 手动save/bgsave命令
- 执行flushall/flushdb命令也会产生dump.rdb文件,但里面是空的,无意义
- 执行shutdown且没有设置开启AOF持久化
- 主从复制时,主节点自动触发
命令:SAVE 和 BGSAVE。前者会阻塞 Redis 服务直到快照过程完成,而后者则是在后台异步进行快照操作。
恢复数据:Redis 启动时自动加载最新的 RDB 文件来恢复数据。
总结:
AOF 持久化
AOF 持久化记录的是服务器接收到的每一个写命令,并且这些命令会在 Redis 重启时重新执行一遍,以此来重建原始数据集。AOF 的优势在于它可以提供更高的数据安全性,因为它几乎可以保证所有成功的写入都会被持久化下来。然而,AOF 文件通常比 RDB 文件大得多,并且重放命令的速度也较慢。
配置:可以通过 appendonly yes 来开启 AOF 功能,并通过 appendfsync 参数控制同步频率(如 always, everysec, no)。
重写机制:为了防止 AOF 文件过大,Redis 支持 AOF 重写功能,即创建一个等价但更小的日志文件来代替旧的日志文件,这个过程同样不会阻塞主进程。
恢复数据:与 RDB 类似,Redis 启动时会根据 AOF 文件来恢复数据。
混合持久化
从 Redis 4.0 版本开始,支持混合持久化模式,它结合了 RDB 和 AOF 的优点。在这种模式下,Redis 会先生成一个 RDB 快照作为基础数据,然后附加后续的 AOF 日志增量。这样既减少了 AOF 文件的大小,又提高了数据的安全性。
配置:可以通过 aof-use-rdb-preamble 参数启用混合持久化。
3.如何保证Redis缓存与数据库如何保证一致性?
四种基础同步策略
同步策略
保证缓存和数据库的双写一致性,共有四种同步策略,即先更新缓存再更新数据库、先更新数据库再更新缓存、先删除缓存再更新数据库、先更新数据库再删除缓存。
-
- 先更新数据库再更新缓存:第二步失败缓存库是旧数据;
- 先删除缓存再更新数据库:第二步失败缓存库是空数据;
- 先更新数据库、再删除缓存(推荐):第二步失败缓存库是旧数据。
更新缓存还是删除缓存?
更新缓存的优缺点
更新缓存的优点是每次数据变化时都能及时地更新缓存,这样不容易出现查询未命中的情况,但这种操作的消耗很大,如果数据需要经过复杂的计算再写入缓存的话,频繁的更新缓存会影响到服务器的性能。如果是写入数据比较频繁的场景,可能会导致频繁的更新缓存却没有业务来读取该数据。
删除缓存的优缺点(推荐)
删除缓存的优点是操作简单,无论更新的操作复杂与否,都是直接删除缓存中的数据。这种做法的缺点则是,当删除了缓存之后,下一次容易出现未命中的情况,那么这时就需要再次读取数据库。
那么对比而言,删除缓存无疑是更好的选择。
先操作数据库还是先删除缓存?
先删除缓存再操作数据库的优缺点
情况1:数据库和缓存内容不一致
线程1删除缓存后还没有来得及更新数据库时,线程2读缓存,由于缓存中的数据已经被线程1清空了所以线程2需要去数据库读数据,然后把读到的结果保存到缓存中。此时线程1更新更新数据库成功。就会出现数据库和缓存内容不一致。
情况2:缓存击穿,数据库卡死
线程1删除缓存后还没有来得及更新数据库时,来了大量的读请求,由于缓存中没有数据,导致缓存击穿直接将大量请求访问到数据库,导致数据库崩溃。
先操作数据库再删除缓存的优缺点(推荐)
脏数据问题:先操作数据库但删除缓存失败的话,导致缓存库里一直存留着旧数据,而我们数据库里存的是新数据。
解决办法:异步重试机制
出现上述问题的时候,我们一般采用重试机制解决,而为了避免重试机制影响主要业务的执行,一般建议重试机制采用异步的方式执行。当我们采用重试机制之后由于存在并发,先删除缓存依然可能存在缓存中存储了旧的数据,而数据库中存储了新的数据,二者数据不一致的情况。
最优同步策略:先更新数据库、再删除缓存
所以我们得到结论:先更新数据库、再删除缓存是影响更小的方案。如果第二步出现失败的情况,则可以采用重试机制解决问题。
同步删除方案: 先更新数据库、再删除缓存。适用于不强制要求数据一致性的情景
流程:先更新数据库、再删除缓存。
问题:
-
- 缓存删除失败:删除失败导致缓存库还是旧数据;
同步删除+可靠消息方案
同步删除+可靠消息删除: 适用于不强制要求数据一致性的情景
流程:先更新数据库、再删除缓存,如果删除失败就发可靠MQ不断重试删除缓存,直到删除成功或重试5次。
问题:MQ多次重试失败,导致长期脏数据。
延时双删:更高一致性方案
延时双删方案:比同步删除策略一致性更高的方案。
流程:先删除缓存再更新数据库,大约在数据库从库更新后再删一次。
问题:时间无法控制,不能保证在数据库从库更新后删除缓存。如果在从库更新前删除,用户再在更新前查从库又把脏数据写在缓存里了。
异步监听+可靠消息删除方案
异步监听+可靠消息删除:很多大厂正在使用的方案。
流程:
-
- Canal等组件监听binlog发现有更新时就发可靠MQ删除缓存;
- 如果删除缓存失败,就基于手动ack、retry等机制,让消息在有限次数之内不断重试。
优点:
-
- 可靠消息重试机制,多次删除保证删除成功。
问题:要求canal等binlog抓取组件高可用,如果canal故障,会导致长期脏数据。
多重保障:最终强一致方案
多重保障方案:同步删除+ 异步监听+可靠消息删除,缓存时设置过期时间,查询时强制主库查;适合于强制要求数据一致性的情况
-
- Canal监听:Canal等组件监听binlog发现有更新时就发可靠MQ删除缓存;第二重保证删缓存成功;
- 延迟消息校验一致性:Canal等组件监听binlog,发延迟MQ,N秒后校验缓存一致性;
- 缓存过期时间:每次缓存时设置过期时间;第三重保证删缓存成功;
- 强制Redis主库查:以后查缓存时强制从缓存主库查;因为主从同步有延迟,同时不用担心主库压力大,因为分片集群机制。