有关保证缓存一致性的问题
1.首先在不考虑并发的情况的下
可以使用facebook的最常用的方法
1.读取数据顺序
读Redis→读MySQL→写Redis(如加一个5分钟的过期时间)
2.写入数据顺序
更新MySQL→删除Redis
这种在没有并发的情况下,可以保证缓存的一致性;
2.考虑到有并发的情况下
我们来列举下有并发的情况下出现的几种并发情况:
读取的并发
1.读Redis→更新MySQL→删除Redis→读MySQL→写Redis
2.读Redis→读MySQL→更新MySQL→删除Redis→ 写Redis (有问题)
写入的并发
3.更新MySQL→读Redis→读MySQL→写Redis→删除Redis
那么就是说当出现并发时,有三分之一的情况下会出现缓存不一致问题,并且不一致的时间会在5分钟后清除,那么怎么解决问题呢.
3.延迟双删
先列出并发问题的情况
读Redis→读MySQL→更新MySQL→删除Redis→ 写Redis (有问题)
我们可以在更新MySQL后加一个延时,也就是
读Redis→读MySQL→更新MySQL→ 写Redis →删除Redis(一定时间的延时如5s后)
延迟可以新开一个线程池,防止阻塞主线程,给用户更好的体验
这样也会有2个问题
1.延迟5S就会在这段时间出现5S的不一致问题,当然我们这个时间也可以更改,设置更短的时间来减少,当然太短的话就做不到延迟的效果了,的自己酌情判断了.
2.如果删除Redis出现了报错,由于处于另一个线程,所以数据库的回滚就不会有效果.
解决方法:
在删除Redis之前在加上一个删除,并于更新MySQL处于同一个线程,这个时候就算Redis宕机也会使事务回滚,最后流程就变为了
读Redis→读MySQL→更新MySQL→删除Redis→ 写Redis →删除Redis(新开线程,一定时间的延时如5s后)
优化方法:
将第一次删除的步骤放到更新前面
读Redis→读MySQL→删除Redis→更新MySQL→ 写Redis →删除Redis(新开线程,一定时间的延时如5s后)
为什么这么做:
如果第一次删除redis宕机了也就是报错了,也就不会触发sql语句的更新,也就不会回滚了,减少了MySQL的损耗,当然我们一般也不会让redis宕机,必要的话建一个redis sentinel集群
@Service
@RequiredArgsConstructor
public class DepartmentService {
private final DepartmentMapper mysqlDao;
private final DepartmentRepository redisDao;
private final ScheduledExecutorService scheduledExecutorService;
public Department getDepartmentById(Long id) {
Department dept = null;
// redis 中有,从 redis 中读取,返回。
dept = redisDao.findById(id).orElse(null);
if (dept != null) {
return dept;
}
// mysql 中也没有,抛出空指针异常
dept = mysqlDao.selectById(id);
if (dept == null)
throw new NullPointerException();
// mysql 中有,但是 redis 中没有。存 redis ,再返回。
redisDao.save(dept);
return dept;
}
public void addDepartment(String name, String location) {
Department dept = new Department(null, name, location);
mysqlDao.insert(dept);
}
public void updateDepartment(Long id, String newName, String newLocation) {
Department dept = new Department(id, newName, newLocation);
// 第一次删除 redis
redisDao.deleteById(id);
// 更新 mysql
mysqlDao.updateById(dept);
// 第二次删除 redis
scheduledExecutorService.schedule(() -> {
redisDao.deleteById(id);
System.out.println("从 redis 中删除成功");
}, 5, TimeUnit.SECONDS);
System.out.println("从 mysql 中删除成功");
}
public void deleteDepartment(Long id) {
// 第一次删除 redis
redisDao.deleteById(id);
// 删除 mysql
mysqlDao.deleteById(id);
// 第二次删除 redis
scheduledExecutorService.schedule(() -> {
redisDao.deleteById(id);
System.out.println("从 redis 中删除成功");
}, 5, TimeUnit.SECONDS);
System.out.println("从 mysql 中删除成功");
}
}
上面的方案还是有设定时间缓存不一致的问题,如果强行要解决就得上锁,损耗性能了
比如加互斥锁,在更新的时候加锁,在读数据库的时候也加锁,这个时候就有数据不一致的问题了,当然在更加严格的情况下,也可以使用读写锁,不仅锁读的部分还锁写的部分
public NurseryManageDto getById(Long varietyId) {
//读锁,由于读锁共享
RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("nurseryManage:"+varietyId);
RLock rLock = readWriteLock.readLock();
rLock.lock(10, TimeUnit.SECONDS);
NurseryManageDto nurseryManageDto = new NurseryManageDto();
RBloomFilter<Object> bloomFilter = redissonClient.getBloomFilter("NurseryManage");
//判断布隆过滤器中是否有该id值
if (!bloomFilter.contains(varietyId)) {
nurseryManageDto.setId(varietyId);
return nurseryManageDto;
}
try {
nurseryManageDto = nurseryManageDtoRedisDao.findById(varietyId).orElseThrow(RedisHaveNotThisDataException::new);
log.info("id为{}的NurseryManageDto从redis中读取", varietyId);
return nurseryManageDto;
} catch (RedisHaveNotThisDataException e) {
//写锁,只有一人可写
RLock writeLock = readWriteLock.writeLock();
writeLock.lock(10, TimeUnit.SECONDS);
try {
//double check 防止有两个用户卡在读取数据区,一个用户读到数据后另一个用户还去数据库中取数据
nurseryManageDto = nurseryManageDtoRedisDao.findById(varietyId).orElseThrow(RedisHaveNotThisDataException::new);
log.info("id为{}的NurseryManageDto从redis中读取", varietyId);
return nurseryManageDto;
} catch (RedisHaveNotThisDataException ex) {
NurseryManage nurseryManage = nurseryManageMysqlDao.selectById(varietyId);
if (nurseryManage == null){
nurseryManageDto = new NurseryManageDto();
nurseryManageDto.setId(varietyId);
nurseryManageDtoRedisDao.save(nurseryManageDto);
return nurseryManageDto;
}
nurseryManageDto = nurseryManageConverter.from(nurseryManage);
//从各个表中获取数据
try {
PlantType plantType = plantTypeRepository.getById(nurseryManage.getPlantTypeId());
nurseryManageDto.setPlantTypeName(plantType.getName());
} catch (Exception eid) {
log.info("该id没有数据");
}
try {
GardenStatusType gardenStatusType = gardenStatusRepository.getById(nurseryManage.getStatusId());
nurseryManageDto.setStatusName(gardenStatusType.getName());
} catch (Exception eid) {
log.info("该id没有数据");
}
try {
PlantBaseDto plantBaseDto = plantBaseRepository.getById(nurseryManage.getBaseId());
nurseryManageDto.setBaseName(plantBaseDto.getName());
} catch (Exception eid) {
log.info("该id没有数据");
}
List<UserDto> userDtoList = userRepository.list();
//将获得的数据存入到DTO中
for (UserDto userDto : userDtoList) {
if (Objects.equals(userDto.getId(), nurseryManageDto.getUpdateUserId())) {
nurseryManageDto.setUpdateUserName(userDto.getName());
} else if (Objects.equals(userDto.getId(), nurseryManageDto.getCreateUserId())) {
nurseryManageDto.setCreateUserName(userDto.getName());
} else if (Objects.equals(userDto.getId(), nurseryManageDto.getMaintainUserId())) {
nurseryManageDto.setMaintainUserName(userDto.getName());
}
}
try {
nurseryManageDtoRedisDao.save(nurseryManageDto);
} catch (Exception er) {
er.printStackTrace();
}
log.info("id为{}的NurseryManageDto从mysql中读取", varietyId);
return nurseryManageDto;
}finally {
writeLock.unlock();
}
}finally {
rLock.unlock();
}
}