1.缓存的使用
1 适合放入缓存的数据:
○ 即时性,数据一致性要求不高
○ 访问量大且更新频率不高的数据(读多,写少)
● 目前将目录查到之后就放在缓存中,以后再查目录的时候如果已经查过了就不需要再查数据库,而是直接从缓存里面拿,避免了再一次查找数据库,加快效率。
● 注意:开发中,放入缓存的数据都应该设置一个过期时间,一方面是使系统即使没有主动更新数据也能触发数据加入缓存,另一方面也是避免程序崩溃的时候,缓存中的数据发了变更,造成数据永久不一致。
2.缓存失效
● 加了缓存之后,在高并发下,也会出现了很多问题,比如缓存雪崩,缓存击穿和缓存穿透。
○ 缓存雪崩:很多key在同一时间失效,造成全部去查数据库,给数据库造成的压力很大。
■ 解决: 设置一个随机的过期时间,不让key在同一时间失效
○ 缓存击穿:对与某个热点key,如果在某一时间失效,这时大量的请求进来查数据库,也会给数据库造成很大的压力。
■ 解决:对于大量的请求同时访问数据库做限制,加锁,只允许一个请求来访问数据库,其余的等待,在第一个请求进入数据库后,后面的时候直接从缓存中拿到数据
○ 缓存穿透:对于数据库中一定不存在的数据(null),缓存中也不存在,那么访问这个不存在的数据每次都会去数据库中查找,造成了不必要的数据库访问。在流量大时,如果有人利用不存在的key频繁攻击服务器,可以会是DB挂了。
■ 解决:将null也存入缓存中,并设置一个较短的过期时间
3.缓存数据一致性
● 定义:数据库中的数据已经更新了,但是缓存中的还没更新,导致用户访问到的还是旧的数据,造成了数据的不一致。
● 解决:
○ 双写模式
■ 更新(写入)数据库的时候,同时更新(写入)缓存,虽然读到的可能有延迟,但能保证最终一致
■ 在并发情况下可能会出现脏数据,但只是暂时的,等缓存过期,下次主动查DB又会变成最新的
●
● 解决:可以加锁,但是不推荐。一般可以接受一段时间的数据不一致
○ 失效模式
■ 更新(写入)数据库的时候,删除缓存,下一次请求会重新读数据库
■
○ 使用binlog订阅canal,当数据库发生变化,canal会对缓存进行修改
●
4.分布式锁的实现
● 本地锁
目前就使用本地锁(sychronized,JUC(lock))来解决缓存击穿的问题
sychronized(this){
Object target = redisTempalte.opsValue(key);
if(target存在){
return target;
}
//查询数据库
//将target放入缓存中
}
但是本地锁只能锁到当前服务实例,在分布式的情况下,每个服务可能部署在多个服务实例上
例如当前,开启四个商品的微服务,分别为10001,10002,10003,10004这几个端口,利用jmeter进行压测,发送500个请求(50个线程,每个线程循环10次),【request–>nigix–>网关(负载均衡)–>4个商品服务实例】,
结果每个服务实例上都进行了一次数据库的查询,但是我们想要的结果是只查一次数据库,不管底层是分布式的还是单体的。
3.分布式锁:
1. redis其实就可以作为分布式锁,当所有的线程进来的时候,首先到redis中进行“占坑”,使用setNX指令,获取到锁之后,就进行业务的执行,没有获取到锁就进行等待,等前一个线程放了再次尝试获取。
一.
Boolean lock = redisTemplate.setIfAbsent("lock","111");
if(lock){
//加锁成功..执行业务
//删除key,释放锁
redisTemplate.delete("lock");
}else{
//加速失败,重试(自旋的方式)
}
问题:这里也会有一个其他的问题,就是当前一个线程获取锁之后发生了异常没有进行释放,就会造成死锁,解决方法是给redis的key设置一个过期时间
Boolean lock = redisTemplate.setIfAbsent("lock","111");
if(lock){
//加锁成功..执行业务
redisTemplate.expire("lock",30,TimeUnit.SECONDS);
//删除key,释放锁
redisTemplate.delete("lock");
}else{
//加速失败,重试(自旋的方式)
}
问题:设置锁和过期时间不是原子性的,还是会出现上面的问题
二.
Boolean lock = redisTemplate.setIfAbsent("lock","111",10,TimeUnit.SECONDS);
if(lock){
if(lock){
//加锁成功..执行业务
//删除key
redisTemplate.delete("lock");
}else{
//加速失败,重试(自旋的方式)
}
问题:在删锁的时候,删的可能不是自己的锁,例如,如果设置锁的过期时间为10,但是改业务已经执行了30s,自己的锁已经过期了,但是现在还能删除,说明删除的是别人占用的锁。
解决:利用UUID设置一个值,在山村的时候进行判断,如果是自己的才进行删除。
三.
Boolean lock = redisTemplate.setIfAbsent("lock",UUID,10,TimeUnit.SECONDS);
if(lock){
//删除key
if(redis.get("lock")==UUID){
redisTemplate.delete("lock");
}
}else{
//加速失败,重试(自旋的方式)
}
问题:redis.get(“lock”) 的网络可能会消耗一定的时间,比如给这个key设置了10s的过期时间,前面的业务执行话了9.5S,在获取原来的值的时候的过程中,过期被另一个线程获取了,这个时候删除的就是别的线程的锁了
四.
String uuid = UUID.randomUUID().toString();
Boolean lock =
redisTemplate.opsForValue().setIfAbsent("lock",uuid,300,TimeUnit.SECONDS);
if(lock){
System.out.println("获取分布式锁成功...");
Map<String, List<Catelog2Vo>> dataFromDb;
try{
dataFromDb = getDataFromDb();
}finally {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1]) else return 0 end";
Long lock1 = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class)
, Arrays.asList("lock"), uuid);
}
return dataFromDb;
}else {
//加锁失败...重试。synchronized ()
//休眠 100ms 重试
System.out.println("获取分布式锁失败...等待重试");
try{
Thread.sleep(200);
}catch (Exception e){
}
return getCatalogJsonFromDbWithRedisLock();//自旋的方式
}
}
获取值对比 + 对比成功操作 保证原子性,使用lua脚本
总结:以上基于redis方式实现的分布式锁,通过使用过期时间,lua脚本等来解决死锁和删除别人的锁的问题,但是还是存在的其他的问题。比对于过期时间的大小选择,如果太小了,会造成提前释放锁,其他业务会提前进来,如果太大了,又会影响要性能。
5.Redision
方便分布式锁的使用,可以向使用JUC那样来使用分布式锁
//获取一把锁,只要锁的名字一样,就是一把锁
RLock lock = redisson.getLock("my-lock");
//加锁
lock.lock();//阻塞等待
try{
//加锁成功,执行业务
}catch(){
}finally(){
//解锁 假设解锁代码没有执行, redision会不会重新死锁
lock.unlock();
}
启动同一个服务的两个实例1,2,在实例1还在执行业务的过程中停掉该服务,这个时候没有实例1没有释放锁
但是实例2仍可以获取锁
看门狗机制:如果业务很长,会在运行期间给锁加上新的30S过期时间,不用担心业务没执行完锁就过期了,加锁的因业务只要执行完,就不会给锁一个过期时间,即使没有释放锁,也会在30S后就释放锁。