第十一章 缓存之更新/穿透/雪崩/击穿

目录

一、什么是缓存

二、缓存更新策略

[2.1. 缓存主动更新策略](#2.1. 缓存主动更新策略)

[2.1.1. Cache Aside模式(主流)‌](#2.1.1. Cache Aside模式(主流)‌)

[2.1.2. Read/Write Through模式‌](#2.1.2. Read/Write Through模式‌)

[2.1‌.3. Write Behind模式‌](#2.1‌.3. Write Behind模式‌)

[2.1.4. 总结](#2.1.4. 总结)

三、缓存穿透

四、缓存雪崩

五、缓存击穿

[5.1. 互斥锁实现](#5.1. 互斥锁实现)

[5.1.1. 实现流程图](#5.1.1. 实现流程图)

[​编辑5.1.2. 主要实现代码](#编辑5.1.2. 主要实现代码)

[5.2. 逻辑过期实现](#5.2. 逻辑过期实现)

六、缓存穿透/击穿封装工具类


本文中的图片内容部分来源于黑马程序员教程案例

一、什么是缓存

‌**‌缓存是一种用于临时存储数据的技术或机制,旨在提高数据访问速度和系统性能。**‌ 缓存通常位于计算机系统内部或附近,可以是硬件、软件或两者的结合体。例如,Web浏览器可以将最常访问的网页内容缓存在本地,以便下次访问时无需从远程服务器重新下载。‌

缓存的作用和原理在于利用程序局部性原理,将频繁访问的数据存储在高速存储器中,如‌SRAM或‌DRAM,以便快速访问。当硬件需要读取数据时,首先在缓存中查找,如果找到则直接执行,否则从内存中查找。由于缓存的运行速度比内存快得多,因此缓存的作用是帮助硬件更快地运行。

缓存可以根据不同的分类标准进行分类。例如,根据存储位置和用途的不同,可以分为‌CPU缓存、‌硬盘缓存、‌内存缓存等。CPU缓存包括‌L1、L2和L3缓存,硬盘缓存用于提高数据传输速度。此外,还有‌HTTP缓存、‌浏览器缓存等。

虽然缓存可以提高系统性能,但也会引入一定的数据一致性问题。因为缓存中的数据可能会与后端数据源(如数据库)存在不一致的情况,所以在使用缓存时需要考虑数据的更新和缓存的过期策略,以保证数据的一致性。

二、缓存更新策略

2.1. 缓存主动更新策略

指在更新数据库中的数据时,如何同步更新缓存中的数据,以保证数据的一致性。常见的缓存更新策略包括‌Cache Aside模式、‌Read/Write Through模式和‌Write Behind模式

2.1.1. **Cache Aside模式(主流)**‌

Cache Aside模式是最常用的缓存更新策略,其中又分为两种:

  1. 先删除缓存,再操作数据库

  2. 先操作数据库,再删除缓存(主流),其操作步骤如下:

  3. 先更新数据库中的数据。

  4. 然后删除缓存中的数据,或者让缓存失效。

  5. 当下次查询时,如果缓存失效,则重新从数据库中读取数据并更新缓存。

注:由于操作缓存和数据库的话存在线程安全性问题,相较于先删除缓存再操作数据库而言,先操作数据库再删除缓存对于产生线程安全性的概率较低,因为写缓存的速度比更新数据库要快很多:线程1在查询缓存未命中后继续查询数据库时,线程2进来更新数据库,绝大部分情况下线程2更新数据库期间,线程1已经完成了缓存的写入。

2.1.2. Read/Write Through模式

Read/Write Through模式将缓存和数据库整合为一个服务,由服务来维护一致性。操作步骤如下:

  1. 查询操作直接从缓存中读取数据。
  2. 如果缓存中不存在数据,则直接从数据库中读取并返回给用户,同时将数据写入缓存。

2.1‌.3. Write Behind模式

Write Behind模式由其他线程异步地将缓存数据持久化到数据库,保证最终一致性。操作步骤如下:

  1. 更新操作只更新缓存。
  2. 由其他线程异步地将缓存数据写入数据库。

2.1.4. 总结

不同策略的优缺点

Cache Aside模式的优点是简单易实现,但存在数据不一致的风险。Read/Write Through模式的优点是数据一致性高,但性能较低。Write Behind模式的优点是最终一致性,但需要额外的线程管理。

不同场景下的适用性

Cache Aside模式适用于读多写少的场景,简单高效。Read/Write Through模式适用于对数据一致性要求高的场景。Write Behind模式适用于写操作较少,可以容忍最终一致性的场景。

缓存更新策略的最佳实践方案

  1. 低一致性需求:使用Redis自带的内存淘汰机制

  2. 高一致性需求:主动更新,并以超时剔除作为兜底方案

读操作:

  • 缓存命中则直接返回
  • 缓存未命中则查询数据库,并写入缓存,设定超时时间

写操作:

  • 先写数据库,然后再删除缓存
  • 要确保数据库与缓存操作的原子性

整个写操作的逻辑,我们要确保事务性,如在单体的Spring的JavaWeb项目业务代码的Service实现层添加@Transactional注解,分布式的项目中可以通过TTC模式来控制,当比如删除Redis缓存出现异常,直接回滚数据库的写操作。

三、缓存穿透

缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会到数据库。

常见的解决方案有两种:

3.1. 缓存空对象(项目中主流用法):

  • 优点:实现简单,维护方便
  • 缺点:额外的内存消耗以及可能造成短期的不一致

2. 布隆过滤器

  • 优点:内存占用较少,没有多余key
  • 缺点:实现复杂、存在误判可能

3. 参数校验过滤不合法请求

通过对用户输入的参数进行校验,例如检查ID的格式是否合法,如果不合法则直接拒绝请求。这种方法可以过滤掉一部分恶意伪造的用户请求,减少对数据库的压力。

4. 使用分布式锁

在缓存未命中时,通过分布式锁控制只有一个请求去查询数据库,其他请求等待锁释放。这样可以防止多个请求同时查询数据库,减轻数据库的压力。

5. 服务降级和限流

在面对大量的恶意请求时,可以通过服务降级或限流的方式来保护后端服务。限流可以限制到达数据库的请求数量,避免数据库压力过大。

四、缓存雪崩

‌**缓存雪崩是指由于缓存中大量数据同时失效或缓存服务器故障,导致大量请求直接打到数据库上,引发数据库压力激增,可能导致整个系统崩溃的现象。**‌ 缓存雪崩通常由于缓存的过期策略不当或缓存服务器故障导致。例如,如果大量的缓存数据设置为在同一时间点过期,或者缓存服务器出现问题无法提供服务,所有的请求将直接访问后端存储系统,导致后端系统瞬时承受巨大的负载压力‌

解决方案:

  • 给不同的key的TTL失效时间设置随机值
  • 利用Redis集群提高服务的可用性
  • 给缓存业务添加降级限流策略
  • 给业务添加多级缓存

五、缓存击穿

‌**缓存击穿是指当一个缓存中的热点数据过期或被删除后,大量并发请求同时到达,导致这些请求直接穿透缓存访问数据库,从而增加数据库的负载。**‌ 这种情况通常发生在热点数据过期或删除的瞬间,导致短时间内大量请求无法通过缓存获取数据,直接访问数据库,造成数据库负载急剧增加‌。

缓存击穿的具体场景包括:

  • 热点数据过期‌:当缓存中的热点数据过期时,大量请求会同时查询后端数据库,导致数据库负载增加‌。
  • 第一次请求‌:对于一个之前从未被请求过的数据,当它第一次被请求时,缓存中没有该数据,导致请求穿透到后端存储‌。

解决缓存击穿的方法包括:

  • 设置热点数据永不过期‌:将热点数据的缓存过期时间设置为较长的时间,甚至是永不过期。这种方法可以避免缓存击穿,但可能导致数据不够及时和准确‌。
  • 使用互斥锁‌:在数据失效时,通过设置互斥锁来保护数据库访问过程。如果某个请求已经获取到了锁,其他请求需要等待,直到获取到锁为止。这种方法可以避免大量并发请求同时访问数据库,但可能导致并发性能下降和请求等待时间增加‌

注意:这两种方案没有孰优孰劣,在实际项目中,我们要针对业务场景和需求,权衡自身是更注重可用性还是一致性来做选择。

5.1. 互斥锁实现

5.1.1. 实现流程图

互斥锁的实现主要基于Redis的命令setnx再附加一个失效时间,key不存在时可以往Redis中写入(返回值1),否则写入失败(返回值0),同一时期如果有多人写入同一个key,只有一人能成功,以此达到互斥锁的效果:

5.1.2. 主要实现代码

java 复制代码
package com.hmdp.service;

import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.baomidou.mybatisplus.extension.service.IService;

public interface IShopService extends IService<Shop> {

    Result queryById(Long id);

    Result update(Shop shop);
}
java 复制代码
package com.hmdp.service.impl;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.hmdp.utils.CacheClient;
import com.hmdp.utils.SystemConstants;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.GeoResult;
import org.springframework.data.geo.GeoResults;
import org.springframework.data.redis.connection.RedisGeoCommands;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.domain.geo.GeoReference;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.util.*;
import java.util.concurrent.TimeUnit;

import static com.hmdp.utils.RedisConstants.*;

@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {


    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public Result queryById(Long id) {
        // 互斥锁解决缓存击穿
        Shop shop = queryWithMutex(id);
        if (shop == null) {
            return Result.fail("店铺不存在");
        }
        return Result.ok(shop);
    }

    @Override
    @Transactional
    public Result update(Shop shop) {
        Long id = shop.getId();
        if (id == null) {
            return Result.fail("id不能为空");
        }
        // 1.更新数据库
        updateById(shop);
        // 2.删除缓存
        stringRedisTemplate.delete(CACHE_SHOP_KEY + id);
        return Result.ok();
    }

    public Shop queryWithMutex(Long id) {
        String key = CACHE_SHOP_KEY + id;
        // 1. 从redis查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        // 2. 判断是否存在
        if (StrUtil.isNotBlank(shopJson)) {
            // 3. 存在,直接返回
            return JSONUtil.toBean(shopJson, Shop.class);
        }
        // 判断命中的是否为null空值,如果是除了null以外的""和" \t\n"这类空串,则直接返回错误信息
        if (shopJson != null) {
            // 返回一个错误信息
            return null;
        }
        // 4. 实现缓存重建
        // 4.1. 获取互斥锁
        String lockKey = "lock:shop" + id;
        Shop shop = null;
        try {
            boolean isLock = trylock(lockKey);
            // 4.2. 判断是否获取成功
            if (!isLock) {
                // 4.3. 失败,则休眠并重试
                Thread.sleep(50);
                return queryWithMutex(id);
            }

            // 4.4 获取成功,则根据id查询数据库
            shop = getById(id);
            // 5. 不存在,则返回错误
            if (shop == null) {
                // 将空值写入redis
                stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
                // 返回错误信息
                return null;
            }
            //  6. 存在,写入Redis
            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            throw new RuntimeException();
        } finally {
            // 释放互斥锁
            unlock(lockKey);
        }
        // 7. 返回
        return shop;
    }

    private boolean trylock(String key) {
        // 不要直接返回flag,因这个值可能会为空,直接拆箱会可能报空指针
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    private void unlock(String key) {
        stringRedisTemplate.delete(key);
    }
}

5.2. 逻辑过期实现

缓存击穿往往发生在高并发访问的热点数据上,因此实际项目中,在我们的预期里,缓存击穿的热点数据是能够命中,即存在的。对于未命中的key,我们认定为非热点数据,因此直接返回空,不做逻辑过期处理。

java 复制代码
package com.hmdp.utils;

import lombok.Data;
import java.time.LocalDateTime;

/**
 * 作为封装的Redis缓存对象
 */
@Data
public class RedisData {
    /**
     * 逻辑过期时间
     */
    private LocalDateTime expireTime;

    /**
     * 缓存的业务实体Bean,如用户、商家等
     */
    private Object data;
}
java 复制代码
package com.hmdp.service;

import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.baomidou.mybatisplus.extension.service.IService;

public interface IShopService extends IService<Shop> {

    Result queryById(Long id);

    Result update(Shop shop);
}
java 复制代码
package com.hmdp.service.impl;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.hmdp.utils.RedisData;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import static com.hmdp.utils.RedisConstants.CACHE_SHOP_KEY;
import static com.hmdp.utils.RedisConstants.LOCK_SHOP_KEY;

@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public Result queryById(Long id) {
        // 逻辑过期
        Shop shop = queryWithLogicalExpire(id);
        if (shop == null) {
            return Result.fail("店铺不存在!");
        }
        // 7.返回
        return Result.ok(shop);
    }

    @Override
    @Transactional
    public Result update(Shop shop) {
        Long id = shop.getId();
        if (id == null) {
            return Result.fail("id不能为空");
        }
        // 1.更新数据库
        updateById(shop);
        // 2.删除缓存
        stringRedisTemplate.delete(CACHE_SHOP_KEY + id);
        return Result.ok();
    }

    /**
     * 逻辑过期代码实现
     * @param id
     * @return
     */
    public Shop queryWithLogicalExpire(Long id) {
        String key = CACHE_SHOP_KEY + id;
        // 1. 从redis查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        // 2. 判断缓存是否命中,未命中表明不是我们的热点数据,直接返回空(实际项目中,在我们的预期里,缓存击穿的热点数据是能够命中的)
        if (StrUtil.isBlank(shopJson)) {
            // 3. 未命中,直接返回空
            return null;
        }
        // 4. 命中,需要把json反序列化为对象
        RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
        Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
        LocalDateTime expireTime = redisData.getExpireTime();
        // 5. 判断是否过期
        if (expireTime.isAfter(LocalDateTime.now())) {
            // 5.1. 未过期则直接返回店铺信息
            return shop;
        }
        // 5.2. 已过期需要缓存重建
        // 6. 缓存重建
        // 6.1. 获取互斥锁
        String lockKey = LOCK_SHOP_KEY +id;
        boolean isLock = trylock(lockKey);
        // 6.2. 判断是否获取锁成功
        if (isLock) {
            // 6.3. 成功,开启独立线程,实现缓存重建
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    this.saveShop2Redis(id, 20L);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    unlock(lockKey);
                }
            });
        }
        // 6.4. 返回过期的商铺信息
        return shop;
    }

    private void saveShop2Redis(Long id, Long expireSeconds) {
        // 1. 查询店铺数据
        Shop shop = getById(id);
        // 2. 封装逻辑过期时间
        RedisData redisData = new RedisData();
        redisData.setData(shop);
        // 当前时间加上传入的指定秒数(即多少秒后过期)
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
        // 3. 写入redis
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
    }

    private boolean trylock(String key) {
        // 不要直接返回flag,因这个值可能会为空,直接拆箱会可能报空指针
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    private void unlock(String key) {
        stringRedisTemplate.delete(key);
    }
}

六、缓存穿透/击穿封装工具类

java 复制代码
package com.hmdp.utils;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

import static com.hmdp.utils.RedisConstants.CACHE_NULL_TTL;
import static com.hmdp.utils.RedisConstants.LOCK_SHOP_KEY;

@Slf4j
@Component
public class CacheClient {

    private final StringRedisTemplate stringRedisTemplate;

    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    public CacheClient(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    public void set(String key, Object value, Long time, TimeUnit unit) {
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
    }

    public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit) {
        // 设置逻辑过期
        RedisData redisData = new RedisData();
        redisData.setData(value);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
        // 写入Redis
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }

    /**
     * 缓存穿透
     * @param keyPrefix
     * @param id
     * @param type
     * @param dbFallback
     * @param time
     * @param unit
     * @param <R>
     * @param <ID>
     * @return
     */
    public <R,ID> R queryWithPassThrough(
            String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit){
        String key = keyPrefix + id;
        // 1.从redis查询商铺缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        // 2.判断是否存在
        if (StrUtil.isNotBlank(json)) {
            // 3.存在,直接返回
            return JSONUtil.toBean(json, type);
        }
        // 判断命中的是否是空值
        if (json != null) {
            // 返回一个错误信息
            return null;
        }

        // 4.不存在,根据id查询数据库
        R r = dbFallback.apply(id);
        // 5.不存在,返回错误
        if (r == null) {
            // 将空值写入redis
            stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            // 返回错误信息
            return null;
        }
        // 6.存在,写入redis
        this.set(key, r, time, unit);
        return r;
    }

    /**
     * 逻辑过期解决缓存击穿
     * @param keyPrefix
     * @param id
     * @param type
     * @param dbFallback
     * @param time
     * @param unit
     * @param <R>
     * @param <ID>
     * @return
     */
    public <R, ID> R queryWithLogicalExpire(
            String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
        String key = keyPrefix + id;
        // 1.从redis查询商铺缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        // 2.判断是否存在
        if (StrUtil.isBlank(json)) {
            // 3.存在,直接返回
            return null;
        }
        // 4.命中,需要先把json反序列化为对象
        RedisData redisData = JSONUtil.toBean(json, RedisData.class);
        R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
        LocalDateTime expireTime = redisData.getExpireTime();
        // 5.判断是否过期
        if(expireTime.isAfter(LocalDateTime.now())) {
            // 5.1.未过期,直接返回店铺信息
            return r;
        }
        // 5.2.已过期,需要缓存重建
        // 6.缓存重建
        // 6.1.获取互斥锁
        String lockKey = LOCK_SHOP_KEY + id;
        boolean isLock = tryLock(lockKey);
        // 6.2.判断是否获取锁成功
        if (isLock){
            // 6.3.成功,开启独立线程,实现缓存重建
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    // 查询数据库
                    R newR = dbFallback.apply(id);
                    // 重建缓存
                    this.setWithLogicalExpire(key, newR, time, unit);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }finally {
                    // 释放锁
                    unlock(lockKey);
                }
            });
        }
        // 6.4.返回过期的商铺信息
        return r;
    }

    /**
     * 互斥锁解决缓存击穿
     * @param keyPrefix
     * @param id
     * @param type
     * @param dbFallback
     * @param time
     * @param unit
     * @param <R>
     * @param <ID>
     * @return
     */
    public <R, ID> R queryWithMutex(
            String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
        String key = keyPrefix + id;
        // 1.从redis查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        // 2.判断是否存在
        if (StrUtil.isNotBlank(shopJson)) {
            // 3.存在,直接返回
            return JSONUtil.toBean(shopJson, type);
        }
        // 判断命中的是否是空值
        if (shopJson != null) {
            // 返回一个错误信息
            return null;
        }

        // 4.实现缓存重建
        // 4.1.获取互斥锁
        String lockKey = LOCK_SHOP_KEY + id;
        R r = null;
        try {
            boolean isLock = tryLock(lockKey);
            // 4.2.判断是否获取成功
            if (!isLock) {
                // 4.3.获取锁失败,休眠并重试
                Thread.sleep(50);
                return queryWithMutex(keyPrefix, id, type, dbFallback, time, unit);
            }
            // 4.4.获取锁成功,根据id查询数据库
            r = dbFallback.apply(id);
            // 5.不存在,返回错误
            if (r == null) {
                // 将空值写入redis
                stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
                // 返回错误信息
                return null;
            }
            // 6.存在,写入redis
            this.set(key, r, time, unit);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }finally {
            // 7.释放锁
            unlock(lockKey);
        }
        // 8.返回
        return r;
    }

    /**
     * 加锁
     * @param key
     * @return
     */
    private boolean tryLock(String key) {
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    /**
     * 解锁
     * @param key
     */
    private void unlock(String key) {
        stringRedisTemplate.delete(key);
    }
}
相关推荐
一 乐2 小时前
民宿|基于java的民宿推荐系统(源码+数据库+文档)
java·前端·数据库·vue.js·论文·源码
山猪打不过家猪3 小时前
(三)总结(缓存/ETag请求头)
缓存·微服务
美林数据Tempodata3 小时前
大模型驱动数据分析革新:美林数据智能问数解决方案破局传统 BI 痛点
数据库·人工智能·数据分析·大模型·智能问数
野槐3 小时前
node.js连接mysql写接口(一)
数据库·mysql
Zzzone6834 小时前
PostgreSQL日常维护
数据库·postgresql
chxii4 小时前
1.13使用 Node.js 操作 SQLite
数据库·sqlite·node.js
冰刀画的圈4 小时前
修改Oracle编码
数据库·oracle
这个胖子不太裤4 小时前
Django(自用)
数据库·django·sqlite
麻辣清汤4 小时前
MySQL 索引类型及其必要性与优点
数据库·mysql
天然首长5 小时前
Redis相关
redis