工具类中实现了get方法的通用接口,针对缓存穿透与缓存击穿问题做了处理。
并且利用setnx命令实现了分布式锁,将加锁与解锁的逻辑利用redis实现,避免了使用互斥锁时多台jvm所拥有的监视锁对象不一致的问题
package com.xiejianjun.tokenlogindemo.utils;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import static com.xiejianjun.tokenlogindemo.utils.RedisConstants.*;
/**
* @author bilibilidick
* @version 2022 06
* @date 2022/6/26 13:07
*/
@Component
public class RedisUtils {
@Resource
private StringRedisTemplate stringRedisTemplate;
private static final DefaultRedisScript UNLOCKSCRIPT;
static {
UNLOCKSCRIPT = new DefaultRedisScript<>();
UNLOCKSCRIPT.setLocation(new ClassPathResource("myredis.lua"));
}
public E getWithMutex(Class clazz, String keyPrefix, ID id, long timeout, TimeUnit unit, Function function) {
E value = null;
try {
// 从redis中查询相应ID的商品信息
String shopJson = stringRedisTemplate.opsForValue().get(keyPrefix + id);
// 查到则直接返回
if (StrUtil.isNotBlank(shopJson))
{
return JSONUtil.toBean(shopJson, clazz);
}
// 未查到则查询MYSQL数据库,查询到则将数据存储到redis中并返回,查不到则返回404
// 为防止缓存穿透,如果未查到数据则缓存空字符串放入redis中
if ("".equals(shopJson)) {
return null;
}
// 如果缓存中不存在这个数据或者缓存已过期,则互斥的进行缓存重建
if (tryLock(CACHE_SHOP_LOCK, id, "1")) {
// 从redis中再次查询相应ID的商品信息
shopJson = stringRedisTemplate.opsForValue().get(keyPrefix + id);
// 查到则直接返回
if (StrUtil.isNotBlank(shopJson))
{
return JSONUtil.toBean(shopJson, clazz);
}
value = function.apply(id);
if (value == null) {
// XXX stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", CACHE_EMPTY_TTL, TimeUnit.MINUTES);
set(keyPrefix + id, "", CACHE_EMPTY_TTL, unit);
return null;
} else {
// XXX stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
set(keyPrefix + id, value, timeout, unit);
}
} else {
Thread.sleep(20);
return getWithMutex(clazz, keyPrefix, id, timeout, unit, function);
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
unLock(CACHE_SHOP_LOCK, id);
}
return value;
}
public void set(String key, E obj, long timeout, TimeUnit unit) {
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(obj), timeout, unit);
}
public boolean tryLock(String keyPrefix, E id, String value) {
Boolean absent = stringRedisTemplate.opsForValue().setIfAbsent(keyPrefix + id, value, CACHE_LOCK_TTL, TimeUnit.SECONDS);
return BooleanUtil.isTrue(absent);
}
public void unLock(String keyPrefix, E id) {
stringRedisTemplate.delete(keyPrefix + id);
}
public void orderLuaUnLock(List lockKey, Object... lockValue) {
stringRedisTemplate.execute(UNLOCKSCRIPT, lockKey, lockValue);
}
}
编写lua脚本保证释放锁的if判断语句与删除键两条命令的原子性,避免if判断语句之后jvm进入fullgc导致的stw进而使redis缓存过期导致的键值误删情况。
---
--- Generated by Luanalysis
--- Created by bilibilidick.
--- DateTime: 2022/6/28 17:51
---
if (redis.call('get', KEYS[1]) == ARGV[1]) then
return redis.call('del', KEYS[1])
end
return 0
实际使用时如果是单机系统,在利用jdk提供的互斥锁时,需要对互斥锁使用intern()方法,避免多个线程创建不同的对象。并且线程互斥代码块需套在事务注解外,避免释放锁后事务未提交的情况,且调用本类方法需利用代理对象调用,因为spring提供的事务注解自己实现的类中是没有的,需要转化为spring的代理类。
package com.xiejianjun.tokenlogindemo.service.impl;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.RandomUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.sun.org.apache.xpath.internal.operations.Or;
import com.xiejianjun.tokenlogindemo.bean.Order;
import com.xiejianjun.tokenlogindemo.bean.Shop;
import com.xiejianjun.tokenlogindemo.bean.User;
import com.xiejianjun.tokenlogindemo.mapper.OrderMapper;
import com.xiejianjun.tokenlogindemo.service.OrderService;
import com.xiejianjun.tokenlogindemo.service.ShopService;
import com.xiejianjun.tokenlogindemo.utils.RedisIdWorker;
import com.xiejianjun.tokenlogindemo.utils.RedisUtils;
import com.xiejianjun.tokenlogindemo.utils.UserHolder;
import com.xiejianjun.tokenlogindemo.vo.Result;
import org.springframework.aop.framework.AopContext;
import org.springframework.aop.framework.AopProxy;
import org.springframework.beans.factory.annotation.Autowired;
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.Collections;
import java.util.concurrent.TimeUnit;
import static com.xiejianjun.tokenlogindemo.utils.RedisConstants.CACHE_ORDER_LOCK;
/**
* @author bilibilidick
* @version 2022 06
* @date 2022/6/28 12:55
*/
@Service
public class OrderServiceImpl extends ServiceImpl implements OrderService {
@Autowired
private ShopService shopService;
@Autowired
private RedisUtils redisUtils;
@Autowired
private RedisIdWorker redisIdWorker;
@Resource
private StringRedisTemplate stringRedisTemplate;
public Result secKillShopV1(long id) {
synchronized (UserHolder.getUser().getId().toString().intern()) {
OrderService proxy = (OrderService) AopContext.currentProxy();
return proxy.syncUser(id);
}
}
@Override
public Result secKillShop(long id) {
OrderService proxy;
String lockValue = UUID.randomUUID(true).toString() + Thread.currentThread().getId();
boolean result = redisUtils.tryLock(CACHE_ORDER_LOCK, UserHolder.getUser().getId().toString(), lockValue);
if (result) {
try {
proxy = (OrderService) AopContext.currentProxy();
return proxy.syncUser(id);
} finally {
String nowLockValue = stringRedisTemplate.opsForValue().get(CACHE_ORDER_LOCK + UserHolder.getUser().getId().toString());
//if (lockValue.equals(nowLockValue)) {
// redisUtils.unLock(CACHE_ORDER_LOCK, UserHolder.getUser().getId().toString());
redisUtils.orderLuaUnLock(Collections.singletonList(CACHE_ORDER_LOCK + UserHolder.getUser().getId()), lockValue);
//}
}
} else {
return Result.error("不要重复下单");
}
}
@Transactional
public Result syncUser(long id) {
Shop targetShop = shopService.getById(id);
if (targetShop == null) return Result.error("商品不存在");
Integer stock = targetShop.getStock();
if (stock > 0) {
int count = count(new QueryWrapper().eq("userid", UserHolder.getUser().getId()));
if (count > 0) return Result.error("不允许重复下单");
int result = shopService.updateStockById(id);
if (result != 0) save(new Order(redisIdWorker.nextTime("order"), UserHolder.getUser().getId(), targetShop.getName(), LocalDateTime.now()));
return Result.ok("秒杀商品"+ targetShop.getName() +"成功");
} else {
return Result.error("商品库存不足");
}
}
}