【redis】加入缓存引发的一系列问题

如上篇博客展示的,三级分类的数据获取吞吐量明显下降,对于分类这种数据,读多写少,我们考虑加入缓存,这样减少和数据库的IO,提升系统性能,但是随之而来的是另一些问题:
1、数据库和缓存怎么保证数据一致性
2、分布式环境下访问同一资源怎么保证数据一致性
3、缓存失效导致的缓存穿透、缓存雪崩、缓存击穿怎么处理

穿透:
查缓存中没有的数据,不命中(解决:允许将null存入缓存)
雪崩:
缓存同时过期,大量请求压到数据库(解决:所有存入缓存的数据加过期时间)
击穿:
高频热点key缓存刚刚过期,百万请求过来查,大量请求压到数据库(解决:加锁,第一个请求获得锁后,查出数据存入缓存,其他请求到缓存查的时候就有数据了)

加锁,加什么锁?本地锁只能锁住当前进程,要想锁多进程必用分布式锁才锁得住。
加锁注意问题:
1、时序问题:要保证入库和入缓存在同一锁内,防止两次查数据库
2、死锁问题:设置过期时间,但要保证加锁和设置锁过期时间的原子性,否则未设置过期时间,redis宕机,锁就删不了
3、锁误删:删锁也要保证原子性,假如设置过期时间10s,A线程业务执行了9.5s,请求redis删锁,在它返回响应前到了过期时间,B线程将锁占用,A执行到对比value发现一致,执行删锁,这时删掉的是B线程的锁,所以使用lua脚本执行删锁,要么执行成功,要么失败。
lua脚本:

if redis.call(“get”,KEYS[1]) == ARGV[1] then
    return redis.call(“del”,KEYS[1])
else
    return 0
end

分布式锁实现要考虑这么多问题,在redis中官方早有推荐Redisson实现分布式锁

//获取锁
RLock myLock = redissonClient.getLock("myLock");
//加锁 阻塞式等待(底层:死循环不断尝试获取锁)
//解决死锁 默认过期时间30s,即使宕机也不会死锁
//myLock.lock();
//redisson看门狗 自动续期解决锁误删问题
//1、锁自动续期 业务超长,到期自动续期30s,不会出现锁自动过期被删问题
//2、加锁的业务运行完成,不会给当前锁续期,即使不手动解锁,默认在30s后自动删除

//10s自动解锁 自动解锁时间一定要大于业务时间 
//问题:myLock.lock(10,TimeUnit.SECONDS);不能续期  
//1、如果传超时时间,会发给redis执行脚本,占锁,默认超时就是我们指定的时间
//2、如果不传超时时间,使用LockWatchdogTimeout看门狗的默认时间30s
//   只要占锁成功,就会启动一个定时任务(重新设置超时时间,就是看门狗的时间)	
//   1/3看门狗的时间,也就是10s以后续期
//建议以下这种方法  省掉续期操作,手动解锁
myLock.lock(10,TimeUnit.SECONDS);
try {
    System.out.println("加锁成功,执行业务"+Thread.currentThread().getId());
    //Thread.sleep(30000);
}catch (Exception e){

}finally {
    //释放锁
    System.out.println("释放锁"+Thread.currentThread().getId());
    myLock.unlock();
}

另外redisson还提供了读写锁:

//保证一定能读到最新数据,修改期间,写锁是一个排他锁,读锁是个共享锁
//写锁没释放就必需等待
// 读+读:相当于无锁,并发读只会redis中
// 写+读:等待写锁释放
// 写+写:阻塞
// 读+写:有读锁 写也要等待
@RequestMapping("/write")
@ResponseBody
public String writeValue(){
    RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("rw-lock");
    RLock rLock = readWriteLock.writeLock();

    String s = "";
    try {
        rLock.lock();
        System.out.println("写锁加锁成功");
        s = UUID.randomUUID().toString();
        Thread.sleep(30000);
        redisTemplate.opsForValue().set("writeValue",s);
    }catch (Exception e){

    }finally {
        rLock.unlock();
        System.out.println("写锁释放");
    }
    return s;
}

@RequestMapping("/read")
@ResponseBody
public String readValue(){
    RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("rw-lock");
    RLock rLock = readWriteLock.readLock();
    rLock.lock();
    String s = "";
    try {
        s = (String) redisTemplate.opsForValue().get("writeValue");
    }catch (Exception e){

    }finally {
        rLock.unlock();
        System.out.println("读锁释放");
    }
    return s;
}

数据库和缓存一致性问题:
无论双写模式还是失效模式都会导致数据不一致的问题,解决方案:
1、保证缓存数据加过期时间,数据过期下次查询会主动更新(缓存+过期时间可解决大部分业务的需求)
2、通过加读写锁解决读写并发,前提是读多写少
3、先删除缓存再更新数据库(这样即使删缓存出错,数据库还未更新,不存在一致性问题)
4、高并发读写问题:先删缓存成功,还未更新数据库,这时又一请求来查redis,为null,去DB将旧数据查出,这时第一个请求更新完DB,两边不一致。
针对亿级流量读写并发:DB与缓存更新与读取串行化
相同商品,将请求路由到同一队列,如果队列已有更新操作(删缓存更新DB)和一个读请求(查DB加缓存),其他读请求要夯时(不用入队列),如果没有不需夯时,在外夯200ms,200ms内读到缓存数据了可以返回,如果没有直接到数据库查。

总结:
我们放入的数据不该是实时性,一致性较高的;
不应该过度设计,增加系统复杂性;
对实时性,一致性要求高的数据,我们宁可查数据库。

你可能感兴趣的:(【Redis入门到精通】)