高并发下的缓存问题解决:本地锁与分布式锁示例

文章介绍了在高并发场景下,如何解决缓存失效问题,如缓存击穿、穿透、雪崩,并给出了从本地锁到Redis分布式锁的解决方案。在本地锁无法满足微服务场景时,转向使用Redis的setNx命令和Redisson客户端实现分布式锁,确保数据一致性。同时,文章讨论了Redisson的看门狗机制,以防止锁自动过期导致的问题。
摘要由CSDN通过智能技术生成

目录

一、本地锁示例:

1、单机服务

2、微服务时

二、Redis实现分布式锁示例

1、setNx

2、Redisson


高并发下的缓存使用:本地缓存与分布式缓存

高并发下的缓存失效问题:缓存击穿、穿透、雪崩

上两篇文章介绍了,缓存的使用以及高并发下缓存失效带来的问题

接下来实际的解决方案。

在高并发下的缓存失效问题:缓存击穿、穿透、雪崩中我们说到缓存击穿、缓存穿透、缓存雪崩的解决方案是:

  • 空结果缓存:解决缓存穿透

  • 设置过期时间(加随机值):解决缓存雪崩

  • 加锁:解决缓存击穿。

那我们在这里进行示例:

未解决之前的代码:

/**
 * 查询所有分类信息从redis缓存中
 *
 * @return
 */
@Override
public Map<String, List<Catelog2Vo>> getCatalogJsonByRedisCache() {

    String catalogJson = redisTemplate.opsForValue().get("catalogJson");
    if (StringUtils.isEmpty(catalogJson)) {
        Map<String, List<Catelog2Vo>> catalogJsonFromDb = getCatalogJsonFromDb();
        redisTemplate.opsForValue().set("catalogJson", JSON.toJSONString(catalogJsonFromDb), 30 * 1000L, TimeUnit.MILLISECONDS);
        return catalogJsonFromDb;
    }
    Map<String, List<Catelog2Vo>> catalogJsonMap = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2Vo>>>() {
    });
    return catalogJsonMap;
}

一、本地锁示例:

加解决方案后:

public Map<String, List<Catelog2Vo>> getCatalogJsonByRedisCacheAndLocalLock() {
    String catalogJson = redisTemplate.opsForValue().get("catalogJsonLocalLock");
    if (StringUtils.isEmpty(catalogJson)) {
        //当查询缓存为空时,尝试加锁
        synchronized (this) {
            //加锁成功后,再进行双重判断,避免不必要的多查一遍数据库
            catalogJson = redisTemplate.opsForValue().get("catalogJsonLocalLock");
            if (StringUtils.isEmpty(catalogJson)) {
                //缓存依旧为空时,再查询数据库放入Redis中
                Map<String, List<Catelog2Vo>> catalogJsonFromDb = getCatalogJsonFromDb();
                //将查询数据放入redis缓存中。(当然整个示例只有这个一个缓存,过期时间就没加随机值了)
                redisTemplate.opsForValue().set("catalogJsonLocalLock", JSON.toJSONString(catalogJsonFromDb), 30 * 1000L, TimeUnit.MILLISECONDS);
                return catalogJsonFromDb;
            }

        }
    }
    Map<String, List<Catelog2Vo>> catalogJsonMap = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2Vo>>>() {
    });
    return catalogJsonMap;
}

1、单机服务

jmeter并发汇总:

服务日志打印:

我们可以从日志中看出,确实只查询了一次数据量。

2、微服务时

jmeter并发汇总

服务日志打印:

8081端口服务打印:

8082端口服务打印:

可以看出,在请求时,两个服务都请求了一次数据库。我们加锁的本意并没有达成。

原因在于synchronized锁,只能作用于当前服务,不能跨服务加锁,在单机服务下,是可以的,但是在微服务情况下无法满足。

这个时候就需要使用到分布式锁。

常见实现分布式锁的方式有:数据库、Redis、Zookeeper。

这其中又以 Redis 最为常见。

二、Redis实现分布式锁示例

Redis官方给提供了加锁的方法,我们先看下,并示例:

1、setNx

代码示例:

/**
     * 查询所有分类信息从redis缓存中加Redis setnx锁
     *
     * @return
     */
    @Override
    public Map<String, List<Catelog2Vo>> getCatalogJsonByRedisCacheAndRedisLockSetNx() {

        String catalogJson = redisTemplate.opsForValue().get("catalogJsonRedisLock");
        Map<String, List<Catelog2Vo>> catalogJsonMap = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2Vo>>>() {
        });
        //判断缓存是否存在
        if (CollectionUtils.isEmpty(catalogJsonMap)) {
            //加锁
            String uuid = UUID.randomUUID().toString();
            Boolean lock = redisTemplate.opsForValue().setIfAbsent("SetNxLock", uuid, 30L, TimeUnit.SECONDS);
            //判断加锁是否成功
            if (Boolean.TRUE.equals(lock)) {
                try {
                    //加锁成功,双重判断
                    catalogJson = redisTemplate.opsForValue().get("catalogJsonRedisLock");
                    log.info("catalogJson: {}", catalogJson);
                    if (StringUtils.isEmpty(catalogJson)) {
                        catalogJsonMap = getCatalogJsonFromDb();
                        redisTemplate.opsForValue().set("catalogJsonRedisLock", JSON.toJSONString(catalogJsonMap), 30 * 1000L, TimeUnit.MILLISECONDS);
                        return catalogJsonMap;
                    }
                    catalogJsonMap = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2Vo>>>() {
                    });
                    return catalogJsonMap;
                } catch (Exception e) {
                    log.error("{}", e.getMessage(), e);
                } finally {
                    //解锁
                    redisTemplate.delete("SetNxLock");
                }

            } else {
                //加锁失败,重试
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                return getCatalogJsonByRedisCacheAndRedisLockSetNx();
            }
        }
        return catalogJsonMap;
    }

继续并发测试,服务的日志打印只查询了一次数据库。(必然的就不贴日志了)。

我们这里测试的是查询数据库的时间实际上是很短的。而我们设置的锁的过期时间是30秒,那这里有一个问题了:

​ 如果,如果业务执行时间(查询数据库时间)超过了30秒,会怎么样呢?

我们来思考下这个过程发生了什么:

  1. 当线程一业务执行时间超过30秒,而锁已过期。这时线程二将拿到锁继续执行,将又执行一遍数据库查询。

  2. 线程二持有锁并且业务未执行完成时,第一个线程已经执行完成,又执行了释放锁的操作。(此时锁的持有者是线程二)

  3. 线程二执行完成,又刷新了一遍缓存,并且也将此时其他线程持有的锁释放。

这里我们能看到两个问题:

  • 业务执行时间超过锁过期时间,锁自动释放。没发续期

  • 锁被超过过期时间的其他线程释放。

如果只是像示例中的分类信息,倒不是特别在意这些。如果是涉及库存、订单及金额的就十分严重了。

首先,第二个问题,redis也给我们提供了解决方案:

示例代码:

/**
     * 查询所有分类信息从redis缓存中加Redis setnx锁
     *
     * @return
     */
    @Override
    public Map<String, List<Catelog2Vo>> getCatalogJsonByRedisCacheAndRedisLockSetNx() {

        String catalogJson = redisTemplate.opsForValue().get("catalogJsonRedisLock");
        Map<String, List<Catelog2Vo>> catalogJsonMap = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2Vo>>>() {
        });
        //判断缓存是否存在
        if (CollectionUtils.isEmpty(catalogJsonMap)) {
            //加锁
            String uuid = UUID.randomUUID().toString();
            Boolean lock = redisTemplate.opsForValue().setIfAbsent("SetNxLock", uuid, 30L, TimeUnit.SECONDS);
            //判断加锁是否成功
            if (Boolean.TRUE.equals(lock)) {
                try {
                    //加锁成功,双重判断
                    catalogJson = redisTemplate.opsForValue().get("catalogJsonRedisLock");
                    log.info("catalogJson: {}", catalogJson);
                    if (StringUtils.isEmpty(catalogJson)) {
                        catalogJsonMap = getCatalogJsonFromDb();
                        redisTemplate.opsForValue().set("catalogJsonRedisLock", JSON.toJSONString(catalogJsonMap), 30 * 1000L, TimeUnit.MILLISECONDS);
                        return catalogJsonMap;
                    }
                    catalogJsonMap = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2Vo>>>() {
                    });
                    return catalogJsonMap;
                } catch (Exception e) {
                    log.error("{}", e.getMessage(), e);
                } finally {
                    //解锁
                    String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
                    //删除锁
                    redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Collections.singletonList("SetNxLock"), uuid);
                }

            } else {
                //加锁失败,重试
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                return getCatalogJsonByRedisCacheAndRedisLockSetNx();
            }
        }
        return catalogJsonMap;
    }

通过脚本对比,当前线程加锁的随机字符串是否一致,一致则删除,否则不做处理。

对于第一个问题,Redis也给我提供了解决方案:redisson

2、Redisson

示例:可参考Redisson官网

第一步:引入maven依赖:

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

第二步:添加Redisson配置类

/**
 * Redisson配置类
 */
@Configuration
public class RedissonConfig {

    @Bean(destroyMethod="shutdown")
    public RedissonClient getRedissonClient(){
        Config config = new Config();
        //单机配置
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
       return Redisson.create(config);
    }
}

第三步:引入依赖

@Autowired
private RedissonClient redissonClient;

第四步:使用

public Map<String, List<Catelog2Vo>> getCatalogJsonByRedisCacheAndRedissonLock() {



    String catalogJson = redisTemplate.opsForValue().get("catalogJsonRedisLock");
    Map<String, List<Catelog2Vo>> catalogJsonMap = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2Vo>>>() {
    });
    if (CollectionUtils.isEmpty(catalogJsonMap)) {
        RLock redissonLock = redissonClient.getLock("redissonLock");
        try {
            redissonLock.lock();
            catalogJson = redisTemplate.opsForValue().get("catalogJsonRedisLock");
            catalogJsonMap = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2Vo>>>() {
            });
            if (CollectionUtils.isEmpty(catalogJsonMap)) {
                catalogJsonMap = getCatalogJsonFromDb();
                redisTemplate.opsForValue().set("catalogJsonRedisLock", JSON.toJSONString(catalogJsonMap), 30 * 1000L, TimeUnit.MILLISECONDS);
                return catalogJsonMap;
            }
            return catalogJsonMap;
        }finally {
            redissonLock.unlock();
        }
    }
    return catalogJsonMap;
}

使用不自定义过期时间(Redisson默认30秒)的锁,就可以解决锁过期问题了。

为了验证锁过期问题是否解决,我们加上打印及时间等待来观察:

public Map<String, List<Catelog2Vo>> getCatalogJsonByRedisCacheAndRedissonLock() {
    
    String catalogJson = redisTemplate.opsForValue().get("catalogJsonRedisLock");
    Map<String, List<Catelog2Vo>> catalogJsonMap = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2Vo>>>() {
    });
    if (CollectionUtils.isEmpty(catalogJsonMap)) {
        RLock redissonLock = redissonClient.getLock("redissonLock");
        try {
            redissonLock.lock();
            log.info("加锁成功");
            try {
                Thread.sleep(33*1000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            catalogJson = redisTemplate.opsForValue().get("catalogJsonRedisLock");
            catalogJsonMap = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2Vo>>>() {
            });
            if (CollectionUtils.isEmpty(catalogJsonMap)) {
                catalogJsonMap = getCatalogJsonFromDb();
                redisTemplate.opsForValue().set("catalogJsonRedisLock", JSON.toJSONString(catalogJsonMap), 30 * 1000L, TimeUnit.MILLISECONDS);
                return catalogJsonMap;
            }
            return catalogJsonMap;
        }finally {
            redissonLock.unlock();
        }
    }
    return catalogJsonMap;
}

日志打印:

我们可以看到,redisson锁的默认过期时间是30秒,而我又加了33秒的睡眠时间,日志打印的两个线程加锁的时间差是33秒,说明锁在业务执行完成之前,并没有过期。已经达到我们想要的结果。

Redisson为什么能达到这样的效果呢?

这依赖于Redisson的看门狗机制。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值