控制层
//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重复写数据