Redis(十八)——缓存与数据库双写不一致问题分析和解决

前言

实际开发中,为了避免频繁查询数据库获取大量信息,造成额外的服务器性能开销和网络延迟问题。一般会增加缓存做数据查询后的临时保存,减少频繁操作数据库耗时问题。

但是,此时却容易出现缓存与数据库双写操作不一致的问题。

什么叫缓存数据库双写不一致?

双写不一致的情况

理想情况下

请求线程1 向数据库写数据,同时更新缓存数据;
线程2在线程1处理完成后,修改数据库,更新缓存。

在这里插入图片描述

此时不会出现缓存数据库双写不一致的问题。

问题出现

但是,由于在实际项目上线后,可能因为分布式环境下,某些服务器GC或其他因素,导致更新数据库后,出现卡顿,并未及时的删除(或更新)缓存信息,此时问题如下所示:
在这里插入图片描述

由于线程1更新缓存操作在线程2更新缓存操作之后进行。
导致数据库中的数据为20,
但缓存中的数据被线程1修改为10。
出现缓存和数据库数据双写不一致的现象!!

更新数据库后删除缓存(存在弊端)

理想情况下

1、线程1向数据库中写数据,写完后删除缓存。
2、线程3随后执行,由于查询到缓存中数据不存在,则从数据库中获取,并更新了缓存。
3、线程2执行,但是删除缓存操作时间在线程3操作完成之后,此时缓存中不会存在脏数据。

在这里插入图片描述

问题出现

但和之前情况一样:

如果线程3因为更新缓存操作延迟,导致更新时间在线程2删除缓存数据之后。
依然会出现双写不一致现象。

在这里插入图片描述
此时依旧出现数据库中数据age为20,但缓存中的数据信息为10的情况!

延迟双删的弊端

网上存在延迟双删的解决策略,但依旧存在问题,如下所示:
在这里插入图片描述
延迟一段时间后,再次执行删除缓存操作。保证缓存中不会出现脏数据。

但是线程2延迟双删时间如何保证?线程3卡顿延迟的时间也具有不确定性!

思维分析

能否像之前探讨的一样,保证每次抢购的逻辑执行为原子性,保证线程1、线程3、线程2等的执行,不被其他线程打断!

采取分布式锁的方式,将并行时容易出现的问题,串行化执行。

分布式处理的思想就是将大量的请求,采取分发处理的思维(负载均衡),让多个服务器共同处理数据,提高处理效率。

但串行化处理高并发问题,导致原本并行处理的思维成了串行,严重降低了分布式处理数据的效率。

Redisson官网中提供了另外一个锁:读 写 锁

读写锁(用于读多写少的业务)

使用读写锁,编写demo并测试。

读锁

import java.util.concurrent.TimeUnit;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RReadWriteLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 读写锁测试
 * @author 
 *
 */
@RestController
public class WriteAndReadController {
	@Autowired
	private Redisson redisson;
	
	@Autowired
	private StringRedisTemplate stringRedisTemplate;
	
	@RequestMapping("/getReadLock")
	public String get() {
		// 设置锁
		String lock = "readLock";
		// 获取读写锁对象
		RReadWriteLock readWriteLock = redisson.getReadWriteLock(lock);
		// 获取读锁
		RLock readLock = readWriteLock.readLock();
		// 尝试加锁
		try {
			// 延迟10秒尝试获取锁,设置锁的生命周期为30秒(默认也是30秒),如果超时依旧还在处理数据,则续命
			boolean tryLock = readLock.tryLock(10,30, TimeUnit.SECONDS);
			// 判断是否拿到了锁
			if(tryLock) {
				// 获取缓存数据
				String stock = stringRedisTemplate.opsForValue().get("stock");
				if(StringUtils.isEmpty(stock) || "0".equals(stock)) {
					System.out.println("数据无,添加数据。。。。");
					// 暂停5秒
					TimeUnit.SECONDS.sleep(5);
					stringRedisTemplate.opsForValue().set("stock", "10");
				}else {
					System.out.println("存在数据。。。。。减少库存");
					Integer intStock = Integer.parseInt(stock);
					intStock = intStock - 10;
					stringRedisTemplate.opsForValue().set("stock", String.valueOf(intStock));
					System.out.println("此时数据为:"+String.valueOf(intStock));
				}
			}
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} finally {
			// 释放锁
			readLock.unlock();
			System.err.println("释放锁。。。。");
		}
		return "end";
	}
}

请求测试:

http://localhost/getReadLock

在这里插入图片描述
发现:

压测读数据时,此时的锁有点形同虚设。

写锁

import java.util.concurrent.TimeUnit;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RReadWriteLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 读写锁测试
 * 
 * @author
 *
 */
@RestController
public class WriteAndReadController {
	@Autowired
	private Redisson redisson;

	@Autowired
	private StringRedisTemplate stringRedisTemplate;

	@RequestMapping("/getWriteLock")
	public String set() {
		// 设置锁
		String lock = "readLock";
		// 获取读写锁对象
		RReadWriteLock readWriteLock = redisson.getReadWriteLock(lock);
		// 获取写锁
		RLock writeLock = readWriteLock.writeLock();
		// 尝试加锁
		try {
			// 每个请求来延迟10秒尝试加锁,如果能加锁则设置时长为30秒,指定时间内未完成操作,则进行续命操作
			boolean tryLock = writeLock.tryLock(10, 30,TimeUnit.SECONDS);
			// 如果拿到锁
			if(tryLock) {
				// 拿到redis中的数据
				String stock = stringRedisTemplate.opsForValue().get("stock");
				System.out.println("拿到锁。。。。此时数据为=="+stock);
				Integer intStock = Integer.parseInt(stock);
				intStock = intStock + 10;
				// 延迟操作
				System.out.println("延迟操作。。。。");
				TimeUnit.SECONDS.sleep(5);
				// 修改redis中的数据信息
				stringRedisTemplate.opsForValue().set("stock", String.valueOf(intStock));
				
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		} finally {
			// 释放锁
			writeLock.unlock();
			System.err.println("释放锁。。。。");
		}
		return "end";
	}
}

在这里插入图片描述
发现:

写锁操作,是串行的;当一个线程拿到锁后执行写操作,其他线程都需要等待其释放锁,才会进行操作!

读锁不互斥,写锁互斥

在读锁源码流程中,org.redisson.RedissonReadLock.tryLockInnerAsync(long, TimeUnit, long, RedisStrictCommand<T>)有一段关键性的代码:
在这里插入图片描述

1、如果请求都是读锁操作,在核心底层中通过lua(原子性),设置key。
2、其他请求来时,判断model是否是读操作,如果是read,则会将key的值累加1
3、如果之前是读锁操作,现在获取到的是写锁且当前key并未释放,此时读锁流程操作将会等待。同时写锁时间续命
4、都是读操作,会同时加锁,同时执行,所以不会造成锁定!

在写锁执行org.redisson.RedissonWriteLock.tryLockInnerAsync(long, TimeUnit, long, RedisStrictCommand<T>)中,其核心代码如下所示:
在这里插入图片描述

1、如果此时是写锁操作且未加锁,则会创建锁,设置mode为write。
2、其他写操作,进入核心代码,判断mode为write,此时新的请求会等待,并将之前的写锁续命。

一般的公司业务,读多写少可以采取读写锁完成加锁操作,保证数据的安全行。

但是当出现读多写多的情况,又和之前设置RedLock、RedissonLock等相似了,都将本来要并行处理的请求串行化,降低了分布式处理数据的效率。

  • 6
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
布隆过滤器是一种非常实用的解决缓存穿透、缓存击穿和缓存雪崩问题的工具。对于缓存穿透问题,布隆过滤器可以在缓存中存储空值,避免频繁查询数据库。布隆过滤器的原理是通过多次哈希运算将元素映射到一个二进制数组中,如果某个位置的值为1,则表示该元素可能存在;如果为0,则表示该元素一定不存在。通过布隆过滤器,可以快速判断一个请求是否需要查询数据库,从而避免了缓存穿透的问题。\[3\] 对于缓存击穿问题,布隆过滤器可以用于限流和降级策略。通过对热点参数进行限流,可以控制请求的并发量,避免数据库被大量请求压垮。同时,对于无效的请求,可以进行服务降级,直接返回默认值或错误信息,而不是查询数据库。\[2\] 对于缓存雪崩问题,布隆过滤器可以作为一种多级缓存解决方案之一。除了使用Redis作为缓存外,还可以使用Nginx缓存等其他缓存工具,将请求分散到不同的缓存层,从而减轻数据库的访问压力。同时,可以通过设置缓存的过期时间,避免大量缓存同时过期,导致数据库访问压力过大。\[2\] 总之,布隆过滤器是一种非常实用的工具,可以有效解决Redis缓存雪崩、缓存穿透和缓存击穿问题。通过合理使用布隆过滤器,可以提高系统的性能和稳定性。 #### 引用[.reference_title] - *1* [redis缓存穿透之终极解决方案——布隆过滤器](https://blog.csdn.net/qq_40606397/article/details/114085367)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [redis缓存雪崩、击穿、穿透](https://blog.csdn.net/weixin_45414913/article/details/124901909)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值