redis高级

redis分布式锁

问题描述

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

分布式锁主流的实现方案:

  1. 基于数据库实现分布式锁
  2. 基于缓存(Redis等)
  3. 基于Zookeeper

​ 每一种分布式锁解决方案都有各自的优缺点

性能:Redis最好

可靠性:Zookeeper最高

Redis锁常见问题

EX second :设置键的过期时间为 second 秒。 SET key value EX second 效果等同于 SETEX key second value 。

PX millisecond :设置键的过期时间为 millisecond 毫秒。 SET key value PX millisecond 效果等同于 PSETEX key millisecond value 。

NX :只在键不存在时,才对键进行设置操作。 SET key value NX 效果等同于 SETNX key value 。

XX :只在键已经存在时,才对键进行设置操作。

设置过期时间未原子操作

通过expire设置过期时间(缺乏原子性:如果在setnx和expire之间出现异常,锁也无法释放)

解决方案:在加锁set时指定过期时间

public Boolean lock(String key, String uuid, Integer time) {
    String value = jedis.set(key, uuid, "NX", "EX", time);
    return value != null;
}

锁误删,可能会释放其他服务器的锁

解决:setnx获取锁时,设置一个指定的唯一值(例如:uuid);释放前获取这个值,判断是否自己的锁在释放锁的时候判断value值是否等于uuid

public void unlock(String key, String uuid) {
    if (StringUtils.equals(uuid, jedis.get(key))) {
        jedis.del(key);
    }
}

锁删除非原子操作

解决方案:lua脚本

定义key,key应该是为每个sku定义的,也就是每个sku有一把锁。

String locKey =“lock:”+skuId; // 锁住的是每个商品的数据
Boolean lock = redisTemplate.opsForValue().setIfAbsent(locKey, uuid,3,TimeUnit.SECONDS);
@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();
        }
    }
}

总结

为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:

  • 互斥性:在任意时刻,只有一个客户端能持有锁
  • 不会发生死锁:即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁
  • 加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
  • 加锁和解锁必须是原子操作

Redis哨兵模式

反客为主的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库

配置哨兵模式

自定义的/myredis目录下新建sentinel.conf文件,名字绝不能错
配置哨兵,填写内容

sentinel monitor mymaster 127.0.0.1 6379 1

其中mymaster为监控对象起的服务器名称, 1 为至少有1个哨兵同意才能迁移
启动哨兵,执行

redis-sentinel /myredis/sentinel.conf

故障恢复

  • 从下线的主服务的所有从服务里面挑选一个从服务,将其转换成主服务

    选择条件依次为:

    1. 选择优先级靠前的
    2. 选择偏移量最大的
    3. 选择runid最小的从服务
  • 挑选出新的主服务之后 sentinel 向原主服务的从服务发送slaveof新主服务的命令,复制新master

  • 当已下线的服务重新上线时,sentinel会向其他发送slaveof命令,让其称为新主的从

当主机挂掉,从机选举中产生新的主机
(大概10秒左右可以看到哨兵窗口日志,切换了新的主机)
哪个从机会被选举为主机呢?
优先级在redis.conf中默认:slave-priority 100,值越小优先级越高
偏移量是指获得原主机数据最全的
每个redis实例启动后都会随机生成一个40位的runid
原主机重启后会变为从机。

复制延时

由于所有的写操作都是先在Master上操作,然后同步更新到Slave上,所以从Master同步到Slave机器有一定的延迟,当系统很繁忙的时候,延迟问题会更加严重,Slave机器数量的增加也会使这个问题更加严重。

posted @ 2025-07-06 21:07  小郑[努力版]  阅读(6)  评论(0)    收藏  举报