版本声明

<dependency>
	<groupId>org.redisson</groupId>
	<artifactId>redisson</artifactId>
	<version>3.34.1</version>
</dependency>

关于Redis工具类

我整合在了基于Springboot3和java17的简单框架|HSXAZJ的博客 (xiake-half.cn)中,有兴趣可以看看

正文

在进行服务端接口编写时,我们时常要考虑如何让接口访问速度更快,如何让数据库的访问压力减小。

最开始,我们引入Redis缓存作为一种手段

最初的模样

public List<User> getUserList(){
    List<User> userList = redisUtil.getList("userList", User.class);
    if(CollectionUtils.isEmpty(userList)){
        List<User> userList = userMapper.selectList(null);
        redisUtil.setList("userList", userList);
    }
    return userList;
}

第一眼看起来毫无问题,先从Redis中获取,当Redis中没有数据时,从数据库中加载,并且缓存到Redis中

这样我们下一次访问这些数据的时候就可以不经过数据库,减轻了数据库的压力

但是

如果此时Redis中没有缓存或者第一个进来的请求还没来得及向Redis写入缓存,又有很多请求同时进来了呢

如图

来自客户端1的请求显然还未完成呢,客户端2,3,4就来访问接口了,而他们发现此时Redis中没有缓存,也齐刷刷地去查询数据库了

在这种情况下,Redis暂时失效了。这在高流量的项目中是非常危险的。

那么如何去解决呢?

加锁

那这个锁要加在哪个位置呢?

把他加在接口的最上面吧,等客户端1把整个流程都走一遍,Redis中就有缓存了,此时客户端2,3,4就可以从Redis中获取数据,好耶!!!

看下代码

public List<User> getUserList() {
    RLock lock = redissonClient.getLock("getUserList");
    List<User> userList;
    try {
        lock.lock();
        userList = redisUtil.getList("userList", User.class);
        if (CollectionUtils.isEmpty(userList)) {
            List<User> userList = userMapper.selectList(null);
            redisUtil.setList("userList", userList);
        }
    } finally {
        lock.unlock();
    }
    return userList;
}

这样Mysql只会被请求一次,好像没什么问题

但是

加锁是会影响性能的呀,每次过来都加锁,数据库是舒服了,但是可能会导致接口请求速度变慢

那把访问Redis的操作移动到锁外面不就好了?

public List<User> getUserList() {
    List<User> userList = redisUtil.getList("userList", User.class);
    if (CollectionUtils.isEmpty(userList)) {
        RLock lock = redissonClient.getLock("getUserList");
        try {
            lock.lock();
            List<User> userList = userMapper.selectList(null);
            redisUtil.setList("userList", userList);
        } finally {
            lock.unlock();
        }
    }
    return userList;
}

这么做的话,你会发现,加了锁之后,数据库不会被并行访问,但访问次数又变回了4次。貌似还不如最初的加锁方案

这可怎么办呢?

诶,客户端1不是已经把缓存加载到Redis中了嘛,那我拿到锁之后再从Redis中获取一遍不就行了?

双检加锁

public List<User> getUserList() {
    List<User> userList = redisUtil.getList("userList", User.class);
    if (CollectionUtils.isEmpty(userList)) {
        RLock lock = redissonClient.getLock("getUserList");
        try {
            lock.lock();
            userList = redisUtil.getList("userList", User.class);
            if (CollectionUtils.isEmpty(userList)) {
                List<User> userList = userMapper.selectList(null);
                redisUtil.setList("userList", userList);
            }
        } finally {
            lock.unlock();
        }
    }
    return userList;
}

拿到锁后,查询数据库前,再从Redis中获取一次数据,说不定前人已经把数据缓存了,这次Redis里要是再没有数据,那就是真没数据了(拿到了锁,其他请求还在后面排队),如果Redis没有,那我再从数据库中去拿。

这个方案的好处是,Redis中有数据时,客户端可以正常并发访问接口,Redis中没有数据时,同一时间进来的多个请求(客户端2,3,4)会在第一个请求(客户端1)结束后从Redis中获取数据,既减轻了数据库压力,也不会对接口的访问速度有大的影响

我是三叶的狗,QQ2279538834