第一步:添加缓存
以若依岗位代码为例
一:首先从redis中查询岗位信息,如果查询到了则直接返回。
二:如果redis中没有数据,则直接从数据库中查询。查询后放到redis并返回
package com.ruoyi.system.service.impl;
import java.util.List;
import com.alibaba.fastjson2.JSON;
import lombok.AllArgsConstructor;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import com.ruoyi.common.constant.UserConstants;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.domain.SysPost;
import com.ruoyi.system.mapper.SysPostMapper;
import com.ruoyi.system.mapper.SysUserPostMapper;
import com.ruoyi.system.service.ISysPostService;
/**
* 岗位信息 服务层处理
*
* @author ruoyi
*/
@Service
@RequiredArgsConstructor
public class SysPostServiceImpl implements ISysPostService
{
private final SysPostMapper postMapper;
private final SysUserPostMapper userPostMapper;
private final StringRedisTemplate redisTemplate;
/**
* 岗位前缀
*/
private static final String POST_KEY = "sys:post:";
/**
* 查询岗位信息集合
*
* @param post 岗位信息
* @return 岗位信息集合
*/
@Override
public List<SysPost> selectPostList(SysPost post)
{
return postMapper.selectPostList(post);
}
/**
* 查询所有岗位
*
* @return 岗位列表
*/
@Override
public List<SysPost> selectPostAll()
{
return postMapper.selectPostAll();
}
/**
* 通过岗位ID查询岗位信息
*
* @param postId 岗位ID
* @return 角色对象信息
*/
@Override
public SysPost selectPostById(Long postId)
{
// 一:从redis中查询缓存是否存在
String postInfo = redisTemplate.opsForValue().get(POST_KEY + postId);
// 二:判定是否存在
if (StringUtils.isNotEmpty(postInfo)){
// 转换为bean
SysPost sysPost = JSON.parseObject(postInfo, SysPost.class);
return sysPost;
}
// 三:如果存在直接返回
SysPost post = postMapper.selectPostById(postId);
// 四:不存在则查询数据库判定数据中是否存在
if (StringUtils.isNull(post)){
return null;
}
redisTemplate.opsForValue().set(POST_KEY + postId, JSON.toJSONString(post));
// 五:如果存在则放到redis中,并返回
return post;
}
/**
* 根据用户ID获取岗位选择框列表
*
* @param userId 用户ID
* @return 选中岗位ID列表
*/
@Override
public List<Long> selectPostListByUserId(Long userId)
{
return postMapper.selectPostListByUserId(userId);
}
/**
* 校验岗位名称是否唯一
*
* @param post 岗位信息
* @return 结果
*/
@Override
public boolean checkPostNameUnique(SysPost post)
{
Long postId = StringUtils.isNull(post.getPostId()) ? -1L : post.getPostId();
SysPost info = postMapper.checkPostNameUnique(post.getPostName());
if (StringUtils.isNotNull(info) && info.getPostId().longValue() != postId.longValue())
{
return UserConstants.NOT_UNIQUE;
}
return UserConstants.UNIQUE;
}
/**
* 校验岗位编码是否唯一
*
* @param post 岗位信息
* @return 结果
*/
@Override
public boolean checkPostCodeUnique(SysPost post)
{
Long postId = StringUtils.isNull(post.getPostId()) ? -1L : post.getPostId();
SysPost info = postMapper.checkPostCodeUnique(post.getPostCode());
if (StringUtils.isNotNull(info) && info.getPostId().longValue() != postId.longValue())
{
return UserConstants.NOT_UNIQUE;
}
return UserConstants.UNIQUE;
}
/**
* 通过岗位ID查询岗位使用数量
*
* @param postId 岗位ID
* @return 结果
*/
@Override
public int countUserPostById(Long postId)
{
return userPostMapper.countUserPostById(postId);
}
/**
* 删除岗位信息
*
* @param postId 岗位ID
* @return 结果
*/
@Override
public int deletePostById(Long postId)
{
return postMapper.deletePostById(postId);
}
/**
* 批量删除岗位信息
*
* @param postIds 需要删除的岗位ID
* @return 结果
*/
@Override
public int deletePostByIds(Long[] postIds)
{
for (Long postId : postIds)
{
SysPost post = selectPostById(postId);
if (countUserPostById(postId) > 0)
{
throw new ServiceException(String.format("%1$s已分配,不能删除", post.getPostName()));
}
}
return postMapper.deletePostByIds(postIds);
}
/**
* 新增保存岗位信息
*
* @param post 岗位信息
* @return 结果
*/
@Override
public int insertPost(SysPost post)
{
return postMapper.insertPost(post);
}
/**
* 修改保存岗位信息
*
* @param post 岗位信息
* @return 结果
*/
@Override
public int updatePost(SysPost post)
{
return postMapper.updatePost(post);
}
}
这是一个简单的添加缓存功能。
第二步:缓存更新策略
主动更新有几种方式?
第一种是常用的
第二种市面上三方的工具少
第三种感觉有点不靠谱。一致性和可靠性都会存在问题,如果redis挂了就出现了一些问题
第三种有疑问其中2种都可以。但是需要具体的分析一下?
先删除缓存,在操作数据库
分析下正常情况:
线程1代表业务人员:业务人员先删除缓存,然后更新数据库
线程2代表客户:客户人员查询缓存未命中,然后写入缓存
非正常情况:
线程1代表业务人员:业务人员先删除缓存。但是由于更新数据库业务复杂,没有更新完。因为没 有加锁的原因
线程2代表客户:在数据库未更新完的情况下,客户点击查询结果缓存中没有直接查询到数据库,
但是数据库还是10,造成了缓存也是10。最终出现了数据库是20,缓存10。
先操作数据库,在删除缓存
正常情况下:
线程2代表业务人员:业务人员更新数据库,然后删除缓存
线程1代表客户:客户查询缓存未命中,在查询数据库写入缓存
非正常业务:这种情况是在
这种情况是线程1查询缓存,缓存正好过期,然后他在查询玩数据库后,准备写入缓存
恰好有一个在更新数据库为20,然后删除缓存,但是那个写入缓存还没开始执行,等他删除缓存后开始执行了。这就造成了脏数据。但是这种情况是在毫秒级别发生的,太需要巧合了。
第三步:案例实现
之前做了岗位的缓存机制:现在实现一个缓存超时剔除和主动更新的策略
1:根据id查询岗位,如果未命中查询数据库,将数据库数据放到缓存里面,并添加超时时间
2:根据id修改岗位是,先修改数据库,在删除缓存
实现超时剔除
/**
* 通过岗位ID查询岗位信息
*
* @param postId 岗位ID
* @return 角色对象信息
*/
@Override
public SysPost selectPostById(Long postId) {
// 一:从redis中查询缓存是否存在
String postInfo = redisTemplate.opsForValue().get(POST_KEY + postId);
// 二:判定是否存在
if (StringUtils.isNotEmpty(postInfo)) {
// 转换为bean
SysPost sysPost = JSON.parseObject(postInfo, SysPost.class);
return sysPost;
}
// 三:如果存在直接返回
SysPost post = postMapper.selectPostById(postId);
// 四:不存在则查询数据库判定数据中是否存在
if (StringUtils.isNull(post)) {
return null;
}
redisTemplate.opsForValue().set(POST_KEY + postId, JSON.toJSONString(post),
RedisConstants.THIRTY_MINUTES,
TimeUnit.MINUTES);
// 五:如果存在则放到redis中,并返回
return post;
}
直接设置过期时间,并且设置为分钟就可以实现缓存自动剔除了。
实现主动更新策略
/**
* 修改保存岗位信息
*
* @param post 岗位信息
* @return 结果
*/
@Override
public int updatePost(SysPost post) {
if (StringUtils.isNull(post.getPostId())) {
throw new ServiceException("id不存在");
}
// 修改岗位信息
int count = postMapper.updatePost(post);
if (count > 0) {
redisTemplate.delete(POST_KEY + post.getPostId());
return count;
}
return 0;
}
这种直接可以实现主动更新策略
结果点击岗位详情,可以看到已经
更新信息的时候则删除缓存了。
第四步:缓存穿透
缓存穿透:当redis和数据库都没有请求的数据时,这样缓存会不生效,所有请求都会打到redis中
解决方案:。2:布隆过滤。
1:缓存空对象
优点:实现简单。可以设置一个时间短的TTL。能解决占内存
缺点:可能站内存。可能会一直给你一个不存在id
2:布隆过滤
优点:内存占用少,少量key。
缺点:不一定准确。
给岗位查询解决缓存穿透的方案:流程图
代码修改:
/**
* 通过岗位ID查询岗位信息
*
* @param postId 岗位ID
* @return 角色对象信息
*/
@Override
public SysPost selectPostById(Long postId) {
// 一:从redis中查询缓存是否存在
String postInfo = redisTemplate.opsForValue().get(POST_KEY + postId);
// 二:判定是否存在
if (StringUtils.isNotEmpty(postInfo)) {
// 转换为bean
SysPost sysPost = JSON.parseObject(postInfo, SysPost.class);
return sysPost;
}
// 这边还有点绕,不是空的,直接返回数据,但是如果不是null说明里面是空字符串就直接返回错误信息
if (StringUtils.isNotNull(postInfo)) {
throw new ServiceException("数据不存在");
}
// 三:如果存在直接返回
SysPost post = postMapper.selectPostById(postId);
// 四:不存在则查询数据库判定数据中是否存在
if (StringUtils.isNull(post)) {
// 然后给控制写入redis
redisTemplate.opsForValue().set(POST_KEY + postId, StringUtils.EMPTY,
RedisConstants.THIRTY_MINUTES,
TimeUnit.MINUTES);
return null;
}
redisTemplate.opsForValue().set(POST_KEY + postId, JSON.toJSONString(post),
RedisConstants.THIRTY_MINUTES,
TimeUnit.MINUTES);
// 五:如果存在则放到redis中,并返回
return post;
}
第五步:缓存雪崩
第六步:缓存击穿
2种方案:互斥锁方案和逻辑删除方案
方案对比:
第七种:利用互斥锁来解决岗位缓存击穿的问题
/**
* 通过岗位ID查询岗位信息
*
* @param postId 岗位ID
* @return 角色对象信息
*/
@Override
public SysPost selectPostById(Long postId) {
// 缓存穿透解决方案
// queryWithPost(postId);
// 缓存击穿
return queryWithMutex(postId);
}
/**
* 缓存击穿问题
* @param postId id
* @return 返回值
*/
@SneakyThrows
private SysPost queryWithMutex(Long postId) {
// 一:从redis中查询缓存是否存在
String postInfo = redisTemplate.opsForValue().get(POST_KEY + postId);
// 二:判定是否存在
if (StringUtils.isNotEmpty(postInfo)) {
// 转换为bean
SysPost sysPost = JSON.parseObject(postInfo, SysPost.class);
return sysPost;
}
// 这边还有点绕,不是空的,直接返回数据,但是如果不是null说明里面是空字符串就直接返回错误信息
if (StringUtils.isNotNull(postInfo)) {
throw new ServiceException("数据不存在");
}
// 尝试获取互斥锁
String lockKey = "lock:post:" + postId;
// 如果失败了,说明可能其他线程在获取取,所以需要休眠一下
Boolean lock = getLock(lockKey);
if (!lock) {
Thread.sleep(NUMBER_1000);
// 递归 在此查询,如果有数据,直接返回
return queryWithMutex(postId);
}
// 三:如果存在直接返回
SysPost post = postMapper.selectPostById(postId);
// 四:不存在则查询数据库判定数据中是否存在
if (StringUtils.isNull(post)) {
// 然后给控制写入redis
redisTemplate.opsForValue().set(POST_KEY + postId, StringUtils.EMPTY,
THIRTY_MINUTES,
TimeUnit.MINUTES);
return null;
}
redisTemplate.opsForValue().set(POST_KEY + postId, JSON.toJSONString(post),
THIRTY_MINUTES,
TimeUnit.MINUTES);
// 五:如果存在则放到redis中,并返回
// 释放锁
deleteLock(lockKey);
return post;
}
/**
* 获取锁
*
* @param key 值
* @return 成功失败
*/
private Boolean getLock(String key) {
return redisTemplate.opsForValue().setIfAbsent(key, String.valueOf(UUID.randomUUID()), THIRTY_MINUTES, TimeUnit.SECONDS);
}
/**
* 是否锁
*
* @param key 值
* @return 成功失败
*/
private Boolean deleteLock(String key) {
return redisTemplate.delete(key);
}
第八种:利用逻辑处理处理岗位缓存击穿问题
这种方案感觉不太好,保证不了数据一致性。