什么是缓存击穿?
缓存击穿是一个热点的Key,有大并发集中对其进行访问,突然间这个Key失效了,导致大并发全部打在数据库上,导致数据库压力剧增。 这种现象就叫做缓存击穿
1、什么是分布式锁
在单机部署的情况下,为了控制多个进程对资源的访问,可以使用同步锁进行互斥控制。但是在分布式系统中,由于分布式系统会有多个线程、多个进程并且分布在不同的机器上,这就使得原来的单机并发控制锁策略失效,所以为了解决这个问题,就有了分布式锁。当多个进程不在同一个系统中,就需要用分布式锁控制多个进程多资源的访问。
2、分布式锁的应用场景
分布式系统中,某个数据(资源)能够同时被多个请求访问,并且修改的场景。
例如:电商系统中对库存的处理,票务系统,首页列表缓存数据的更新等的
3、分布式锁的失效场景
4、分布式锁node.js实践
语言:node.js
数据库版本:mongodb: "4.1.2"
redis版本:4.0.0
//lua脚本
let script = "if redis.call('get',KEYS[1]) == ARGV[1] then" +
" return redis.call('del',KEYS[1]) " +
"else" +
" return 0 " +
"end";
//先查redis缓存
let resultString = await redisCmd('get')('testData')
//定义返回数据对象
let participantAndVoteCountData
if (resultString !== null) {
//命中缓存
console.log("successful!");
return response(0, JSON.parse(resultString))
}
//缓存未命中,查询数据库
for (let i = 0; i < 3; i++) {
//获取锁 生成uuId保证每个锁的唯一性
let uuId = await uuidv1()
console.log('uuId:', uuId)
let addLock = await redisCmd('set')('lock', uuId, 'EX', 2, 'NX')
console.log("addLock:", addLock)
if (addLock) {
//获取成功,重新查询redis,防止加锁的同时缓存已经被更新
resultString = await redisCmd('get')('testData')
console.log('resultString:', resultString)
//字符串转换成对象
participantAndVoteCountData = JSON.parse(resultString)
if (!participantAndVoteCountData) {
//未命中缓存
//返回数据赋值
participantAndVoteCountData = await getData()
await redisCmd('set')('testData', JSON.stringify(testData), 'EX', 10)
console.log('testData:', testData)
}
//命中缓存
//释放锁
await redisClient.eval(script, 1, 'lock', uuId)
return response(0, testData)
}
//未能竞争到锁,等待再次获取锁
await wait(50)
}
return response(10030)
如以上代码为例:
这段代码主要是使用redis分布式锁避免redis缓存击穿问题。
首先我们先去获取redis中的key,如果能过成功读取到value则成功返回就不需要进行其他操作了。
当key缓存过期的情况的时候,首先我们需要给请求加一个锁,因为在这个过期的时间节点呢会存在很多请求同时访问redis,若不进行加锁的操作的话,就会形成缓存击穿的问题,他们同时去数据库进行查询。
首先我们给每个请求都生成一个uuid的唯一标识作为锁的value值,redisCmd('set')('lock', uuId, 'EX', 2, 'NX')
加锁,进行判断。如果addLock
存在(如果加锁成功返回’OK’)则说明加锁成功。
首先说加锁成功的时候,加锁成功不意味着我们立马去读数据库取数据,因为我们并不知道我们是不是这么多请求里面第一个获取锁的请求,如果说我们不是第一个获取锁的请求的话那么其他的请求肯定已经更新了redis了,那么就没有必要再去操作一遍数据库了。所以我们此时要多一个步骤就是再次读redis,看看此时redis是否能过读出值来,如果有值,则我们直接取值,把锁给释放掉,然后返回。如果没有值的话,那么此时说明我们是第一个拿到锁的,那么去数据库里面读取相关数据的操作我们肯定是当仁不让的来。
最后就是释放锁了,释放锁不能直接就把key给进行删除了,因为我们并不能知道你删除的是不是自己锁,比如我们设置的过期时间是2秒,但是在我们准备释放锁的时候已经过期了,那么此时我们直接删除key的话不就把别人的锁给删除了。 这里我们要确保释放的锁是自己的锁需要用到lua表达式,redisClient.eval(script, 1, 'lock', uuId)
这样我们就可以确保了我们不会进行误删锁的操作。
此外:如果在一开始我们没有获取锁成功怎么办呢,在上面的代码中我是再让他循环了几次获取锁,在一起别的场景中可能就会用到自旋的操作,一定会让进程获取锁,所以有这样的需求的话把for改为while循环更好一些。
还有很多不足的地方,希望能过一起学习探讨。接下来也会将redis分布式锁的一些失效场景一一举例写下来