有关保证缓存一致性的问题

有关保证缓存一致性的问题

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();
        }
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值