文章目录

  • 前言
  • 双写不一致的情况
  • 理想情况下
  • 问题出现
  • 更新数据库后删除缓存(存在弊端)
  • 理想情况下
  • 问题出现
  • 延迟双删的弊端
  • 思维分析
  • 读写锁(用于读多写少的业务)
  • 读锁
  • 写锁
  • 读锁不互斥,写锁互斥


前言

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

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

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

双写不一致的情况

理想情况下

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

sping配置redis双写 redis双写不一致问题_缓存

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

问题出现

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

sping配置redis双写 redis双写不一致问题_数据_02

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

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

理想情况下

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

sping配置redis双写 redis双写不一致问题_缓存_03

问题出现

但和之前情况一样:

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

sping配置redis双写 redis双写不一致问题_sping配置redis双写_04


此时依旧出现数据库中数据age为20,但缓存中的数据信息为10的情况!

延迟双删的弊端

网上存在延迟双删的解决策略,但依旧存在问题,如下所示:

sping配置redis双写 redis双写不一致问题_sping配置redis双写_05


延迟一段时间后,再次执行删除缓存操作。保证缓存中不会出现脏数据。

但是线程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

sping配置redis双写 redis双写不一致问题_sping配置redis双写_06


发现:

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

写锁

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";
	}
}

sping配置redis双写 redis双写不一致问题_数据_07


发现:

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

读锁不互斥,写锁互斥

在读锁源码流程中,org.redisson.RedissonReadLock.tryLockInnerAsync(long, TimeUnit, long, RedisStrictCommand<T>)有一段关键性的代码:

sping配置redis双写 redis双写不一致问题_redis_08

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

在写锁执行org.redisson.RedissonWriteLock.tryLockInnerAsync(long, TimeUnit, long, RedisStrictCommand<T>)中,其核心代码如下所示:

sping配置redis双写 redis双写不一致问题_数据_09

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

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

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