只查数据库一次,后面访问都查Redis缓存实现

控制层 

    //3. 根据Id查询部门员工信息
    @GetMapping("/{id}")
    public Object findEmpById(@PathVariable Integer id) {
        //多线程模拟并发访问
        ExecutorService es = Executors.newFixedThreadPool(200);
        for (int i = 0; i < 500; i++) {
            es.submit(new Runnable() {
                @Override
                public void run() {
                    empService.findEmpById(id);
                }
            });
        }
        return empService.findEmpById(id);
    }

 业务层

@Service
@Slf4j
public class EmpServiceImpl implements EmpService {

    @Autowired
    private EmpMapper empMapper;

    @Autowired
    private RedisTemplate redisTemplate;
//========================================================
  
    /**
     * 根据Id查询部门员工信息
     * 使用双重检测机制加锁,解决并发问题
     * @param id
     * @return
     */
    @Override
    public Object findEmpById(Integer id) {

        String key = "user:" + id;

        //先从缓存获取数据
        Object userObj = redisTemplate.opsForValue().get(key);

        if (userObj == null) {  //这里可以使用工具类判断
            //1. 如果没有,则查询mysql数据库,并且将数据存入Redis缓存
            synchronized (this.getClass()) {
                //关键核心再次判断
                userObj = redisTemplate.opsForValue().get(key);
                if (userObj == null) {      
                    log.info("userObj:{}",userObj);
                    Emp emp = empMapper.selectByPrimaryKey(id);
                    redisTemplate.opsForValue().set(key, emp);
                    userObj = emp;
                    return emp;
                }else {
                    return userObj;
                }
            }
        } else {
            //2. 如果Redis缓存有数据则直接返回
            return userObj;
        }
    }
}

后续补充:

package com.hmdp.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.constant.RedisConstant;
import com.hmdp.dto.RedisData;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.hmdp.utils.JacksonUtil;
import com.hmdp.utils.RedisUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundValueOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
 * User: ldj
 * Date: 2023/4/1
 * Time: 15:23
 * Description: 缓存穿透还可以做入参id检验,主动规避不合法请求
 */
@Slf4j
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public Result queryShopById(Long id) {
        Shop shop = concurrentQueryShopById(id);
        //Shop shop = queryShopWithLogicalExpire(id);
        if (Objects.isNull(shop)) {
            return Result.fail("404:店铺不存在!");
        }
        return Result.ok(shop);
    }

    @Override
    public Result updateShop(Shop shop) {
        Long shopId = shop.getId();
        if (shopId == null) {
            return Result.fail("店铺id不能为空");
        }
        //更新数据库并删除缓存
        this.updateById(shop);
        String redisKey = RedisConstant.REDIS_SHOP_PREFIX + shopId;
        stringRedisTemplate.delete(redisKey);
        return Result.ok();
    }

    private Shop concurrentQueryShopById(Long id) {
        //尝试从redis获取店铺数据,存在直接返回数据
        String redisKey = RedisConstant.REDIS_SHOP_DOUBLE_PREFIX + id;
        BoundValueOperations<String, String> operations = stringRedisTemplate.boundValueOps(redisKey);
        String redisShop = operations.get();
        log.info("redisShop:{}", redisShop);

        //数据库没有数据,缓存为"",防穿透
        if ("".equals(redisShop)) {
            log.warn("缓存空命中!");
            return null;
        }

        if (StringUtils.isNotBlank(redisShop)) {
            return JacksonUtil.readValue(redisShop, Shop.class);
        }

        //双检+锁控制并发,防缓存击穿
        synchronized (this.getClass()) {
            redisShop = operations.get();
            if (redisShop == null) {
                log.info("缓存未命中,查询数据库 shopId:[{}]", id);
                Shop shop = this.getById(id);
                if (Objects.isNull(shop)) {
                    operations.setIfAbsent("", RedisConstant.REDIS_NULL_TTL, TimeUnit.SECONDS);
                    return null;
                }
                String redisValue = JacksonUtil.writeValueAsString(shop);
                log.info("shopJson:{}", redisValue);
                operations.setIfAbsent(redisValue, RedisConstant.REDIS_SHOP_TTL, TimeUnit.MINUTES);
                return shop;
            }
            return JacksonUtil.readValue(redisShop, Shop.class);
        }
    }

    /**
     * 逻辑过期解决缓存击穿
     */
    private Shop queryShopWithLogicalExpire(Long id) {
        //从缓存获取数据
        String redisKey = RedisConstant.REDIS_SHOP_PREFIX + id;
        RedisData redisData = getRedisData(redisKey);
        if (Objects.isNull(redisData)) {
            return null;
        }

        Shop shop = getShop(redisData);

        //缓存有数据且没过期,直接返回
        if (!isExpired(redisData)) {
            String logicalExpireTime = redisData.getExpireTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
            log.info("[Redis] 缓存尚未过期 logicalExpireTime:[{}]", logicalExpireTime);
            return shop;
        }

        //缓存过期,查询数据库并更新缓存 使用分布式互斥锁防线程并发
        Boolean isLocked = RedisUtil.getLock();
        if (isLocked) {
            Shop shopFromDB = null;
            try {
                shopFromDB = this.getShopFromDB(id);
                this.shopSaveRedis(id, shopFromDB);
            } catch (Exception e) {
                log.error(e.getMessage());
            } finally {
                RedisUtil.releaseLock();
            }
            return shopFromDB;
        }

        //缓存过期但又枪不到锁,兜底旧数据
        log.warn("兜底数据");
        return shop;
    }

    private Shop getShop(RedisData redisData) {
        Shop shop = JacksonUtil.readValue(redisData.getData(), Shop.class);
        if (Objects.isNull(shop)) {
            return null;
        }
        return shop;
    }

    private RedisData getRedisData(String redisKey) {
        String redisDataJson = stringRedisTemplate.opsForValue().get(redisKey);
        if (redisDataJson == null) {
            return null;
        }

        //反序列化失败返回null
        RedisData redisData = JacksonUtil.readValue(redisDataJson, RedisData.class);
        if (Objects.isNull(redisData)) {
            return null;
        }
        return redisData;
    }

    private Shop getShopFromDB(Long id) {
        log.info("缓存过期,查询数据库 shopId:[{}]", id);
        Shop shop = this.getById(id);
        if (Objects.isNull(shop)) {
            return null;
        }
        return shop;
    }

    private void shopSaveRedis(Long id, Shop shop) {
        if (Objects.isNull(shop)) {
            throw new RuntimeException("店铺不存在!");
        }

        RedisData redisData = new RedisData();
        redisData.setData(shop);
        //刷新逻辑缓存时间 (测试期间时间短)
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(30));
        String redisKey = RedisConstant.REDIS_SHOP_PREFIX + id;
        String redisValue = JacksonUtil.writeValueAsString(redisData);
        stringRedisTemplate.opsForValue().set(redisKey, redisValue);
    }

    //ture:过期 false:未过期
    private boolean isExpired(RedisData redisData) {
        LocalDateTime expireTime = redisData.getExpireTime();
        return LocalDateTime.now().isAfter(expireTime);
    }

}

这里使用setIfAbsent() 可以防止服务集群部署时,每台服务线程都来Redis重复写数据

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值