文章目录
- 前言
- 双写不一致的情况
- 理想情况下
- 问题出现
- 更新数据库后删除缓存(存在弊端)
- 理想情况下
- 问题出现
- 延迟双删的弊端
- 思维分析
- 读写锁(用于读多写少的业务)
- 读锁
- 写锁
- 读锁不互斥,写锁互斥
前言
实际开发中,为了避免频繁查询数据库获取大量信息,造成额外的服务器性能开销和网络延迟问题。一般会增加缓存做数据查询后的临时保存,减少频繁操作数据库耗时问题。
但是,此时却容易出现缓存与数据库双写操作不一致的问题。
什么叫缓存数据库双写不一致?
双写不一致的情况
理想情况下
请求线程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等相似了,都将本来要并行处理的请求串行化,降低了分布式处理数据的效率。