09-集群jedis开发与应用问题解决

目录

一、集群jedis开发

二、Redis集群的好处

三、Redis集群的缺点

四、缓存穿透

五、缓存击穿

六、缓存雪崩

七、分布式锁

解决方案

UUID防止误删

LUA保证删除原子性

总结


一、集群jedis开发


即使连接的不是主机,集群会自动切换主机存储。主机写,从机读
无中心化主从集群。无论从哪台主机写的数据,其他主机上都能读到数据
 

public class JedisClusterTest {
    public static void main(String[] args) {
        Set<HostAndPort> set =new HashSet<HostAndPort>();
        set.add(new HostAndPort("192.168.31.211",6379));
        JedisCluster jedisCluster=new-JediscCluster(set);
        jedisCluster.set("k1","v1");
        System.out.println(iedisCluster.get("k1");
    }
}

二、Redis集群的好处

  • 实现扩容
  • 分摊压力
  • 无中心配置相对简单

三、Redis集群的缺点

  • 多键操作是不被支持的
  • 多键的Redis事务是不被支持的。lua 脚本不被支持
  • 由于集群方案出现较晚,很多公司已经采用了其他的集群方案,而代理或者客户端分片的方案想要迁移至redis cluster ,需要整体迁移而不是逐步过渡,复杂度较大
     

四、缓存穿透

key对应的数据在数据源并不存在,每次针对此key的请求从缓存获取不到,请求都会压到数据源,从而可能压垮数据源。比如用一个不存在的用户id 获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库。
 

问题:

1.应用服务器压力变大了(大量请求)

2.redis命中率降低(一直查不到数据)

3.一直查询数据库

 原因:

1.redis查询不到数据库

2.出现很多非正常的url访问(恶意攻击

一个一定不存在缓存及查询不到的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义


解决方案:
(1) 对空值缓存:如果一个查询返回的数据为空(不管是数据是否不存在) ,我们仍然把这个空结果( null )进行缓存,设置空结果的过期时间会很短,最长不超过五分钟。(临时应急方案
(2)设置可访问的名单 (白名单) :使用bitmaps类型定义一个可以访问的名单,名单id作为bitmaps的偏移量,每次访问和bitmap里面的id进行比较,如果访问id不在bitmaps里面,进行拦截,不允许访问(每次访问都要访问bitmaps效率不高
(3) 采用布隆过滤器: (布隆过滤器( Bloom Filter) 是1970 年由布隆提出的。它实际上是一个很长的二进制向量(位图)和一系列随机映射函数 (哈希函数)(命中率可能不够准确

 (4) 进行实时监控:当发现Redis的命中率开始急速降低,需要排查访问对象和访问的数据,和运维人员配合,可以设置黑名单限制服务

五、缓存击穿

问题:

1.数据库访问压力瞬时增加

2.redis里面没有大量key过期

3.redis正常运行

原因:

1.redis某个key过期,但这个key有大量的访问。(热门key)

key可能会在某些时间点被超高并发地访问,是-种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题

解决问题:
(1 )预先设置热门数据:在redis 高峰访问之前,把一 些热]数据提前存入到redis里面,加大这些热门数据key的时长。
(2)实时调整:现场监控哪些数据热门,实时调整key的过期时长
(3)使用锁:(效率低)
    

六、缓存雪崩

 

问题:

1.数据库压力变大,服务器奔溃

原因:

1.在极少时间段,查询大量key的集中过期情况

缓存失效时的雪崩效应对底层系统的冲击非常可怕!


解决方案:
(1) 构建多级缓存架构: nginx缓存+ redis缓存+基他缓存( ehcache等)
(2) 使用锁或队列:(最有效,但效率低
用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。不适用高并发情况
(3) 设置过期标志更新缓存:记录缓存数据是否过期(设置提前量) , 如果过期会触发通知另外的线程在后台去更新实际key的缓存
(4) 将缓存失效时间分散开:比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5 分钟随机,这样每一一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件
 

 七、分布式锁

随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制钬策略失效,单纯的Java API并不能提供分布式锁的能力。为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题! 

分布式锁主流的实现方案:
1.基于数据库实现分布式锁
2.基于缓存(Redis等)
3.基于Zookeepers

每一种分布式锁解决方案都有各自的优缺点:
1.性能: redis最高
2.可靠性: zookeeper最高

这里,我们就基于redis实现分布式锁
 

解决方案

实现redis分布式锁

redis:命令
set sku:1:info“OK" NX PX 10000
EX second :设置键的过期时间为second秒。

SET key value EX second效果等同于SETEX key second value
 

setnx 设置锁

1.
setnx users 10
setnx users 20
第二次不能设置了,因为加了锁

2.
del users
setnx users 20
才能设置

如果锁一直不释放,就无法进行下去

可以设置过期时间

expire users 10

设置10秒过期

就可以继续

setnx users 30

这个操作并不是原子操作,因此

在上锁之后突然异常,无法设置过期时间

解决:上锁同时设置时间

set users 10 nx ex 12

nx表示上锁

ex表示12秒过期

代码:

@GetMapping("testLock")
    public void testLock(){
        String uuid = UUID.randomUUID().toString();
        //1获取锁,setne,3秒是过期时间
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid,3, TimeUnit.SECONDS);
        //2获取锁成功、查询num的值
        if(lock){
            Object value = redisTemplate.opsForValue().get("num");
            //2.1判断num为空return
            if(StringUtils.isEmpty(value)){
                return;
            }
            //2.2有值就转成成int
            int num = Integer.parseInt(value+"");
            //2.3把redis的num加1
            redisTemplate.opsForValue().set("num", ++num);
            //2.4释放锁,del
            //判断比较uuid值是否一样
            String lockUuid = (String)redisTemplate.opsForValue().get("lock");
            if(lockUuid.equals(uuid)) {
                redisTemplate.delete("lock");
            }
        }else{
            //3获取锁失败、每隔0.1秒再获取
            try {
                Thread.sleep(100);
                testLock();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

测试该段代码:

ab -n 1000 -c 100 http://192.168.2.80:8080/redisTest/testLock

锁被其他主机释放问题

解决优化:UUID防止误删

UUID防止误删

解决分布式导致锁误删的问题

第一步:uuid表示不同的操作

set lock uuid nx ex 10

第二步:释放锁时,先判断当前uuid和释放锁的uuid是否一致

代码:

1.在释放前加一个UUID生成
String uuid = UUID.randomUUID().toString();

2.其中传入uuid这个参数
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid,3, TimeUnit.SECONDS);

3.释放前判断UUID是否一样
            //2.4释放锁,del
            //判断比较uuid值是否一样
            String lockUuid = (String)redisTemplate.opsForValue().get("lock");
            if(lockUuid.equals(uuid)) {
                redisTemplate.delete("lock");
            }

LUA保证删除原子性

Lua脚本是什么?

LUA脚本是类似redis事务,有一定的原子性,不会被其他命令插队,可以完成一些redis事务性的操作

代码:

@GetMapping("testLockLua")
    public void testLockLua() {
        //1 声明一个uuid ,将做为一个value 放入我们的key所对应的值中
        String uuid = UUID.randomUUID().toString();
        //2 定义一个锁:lua 脚本可以使用同一把锁,来实现删除!
        String skuId = "25"; // 访问skuId 为25号的商品 100008348542
        String locKey = "lock:" + skuId; // 锁住的是每个商品的数据

        // 3 获取锁
        Boolean lock = redisTemplate.opsForValue().setIfAbsent(locKey, uuid, 3, TimeUnit.SECONDS);

        // 第一种: lock 与过期时间中间不写任何的代码。
        // redisTemplate.expire("lock",10, TimeUnit.SECONDS);//设置过期时间
        // 如果true
        if (lock) {
            // 执行的业务逻辑开始
            // 获取缓存中的num 数据
            Object value = redisTemplate.opsForValue().get("num");
            // 如果是空直接返回
            if (StringUtils.isEmpty(value)) {
                return;
            }
            // 不是空 如果说在这出现了异常! 那么delete 就删除失败! 也就是说锁永远存在!
            int num = Integer.parseInt(value + "");
            // 使num 每次+1 放入缓存
            redisTemplate.opsForValue().set("num", String.valueOf(++num));
            /*使用lua脚本来锁*/
            // 定义lua 脚本
            String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
            // 使用redis执行lua执行
            DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
            redisScript.setScriptText(script);
            // 设置一下返回值类型 为Long
            // 因为删除判断的时候,返回的0,给其封装为数据类型。如果不封装那么默认返回String 类型,
            // 那么返回字符串与0 会有发生错误。
            redisScript.setResultType(Long.class);
            // 第一个要是script 脚本 ,第二个需要判断的key,第三个就是key所对应的值。
            redisTemplate.execute(redisScript, Arrays.asList(locKey), uuid);
        } else {
            // 其他线程等待
            try {
                // 睡眠
                Thread.sleep(1000);
                // 睡醒了之后,调用方法。
                testLockLua();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

Lua脚本;

/*使用lua脚本来锁*/
            // 定义lua 脚本
            String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
            // 使用redis执行lua执行
            DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
            redisScript.setScriptText(script);
            // 设置一下返回值类型 为Long
            // 因为删除判断的时候,返回的0,给其封装为数据类型。如果不封装那么默认返回String 类型,
            // 那么返回字符串与0 会有发生错误。
            redisScript.setResultType(Long.class);
            // 第一个要是script 脚本 ,第二个需要判断的key,第三个就是key所对应的值。
            redisTemplate.execute(redisScript, Arrays.asList(locKey), uuid);

总结

为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:
互斥性。在任意时刻,只有一个客户端能持有锁
不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁
解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了
加锁和解锁必须具有原子性
 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ZuckD

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值