redis分布式锁

redis分布式事务

概述:

场景:多系统(多进程)对同一资源并发修改操作。PS:同一进程的多线程调用是系统内事务管理不属于分布式事务。
思路:通过设置唯一锁,判断是否又其他客户端在使用。redis中可以通过setNx(key)来上锁,get(key)检查是否被上锁。
难点:保证锁的唯一性、原子性、高可用性、容错性和避免死锁。

思考

基于分布式事务产生的的场景进行考虑解决方案
    1、在多系统并发操作:选择合适的中间件(跨系统)、数据库(底层)。
    2、容错性:就要确保在提供服务方的稳定、容错性,应该提供集群的概念。
    3、高效:redis、rocketMQ、zookeeper|mysql。
    4、集合事务本质和所掌握的中间件进行思考,技术的出现是为了解决业务难题。

实现方式:

1、redis
2、zookeper:
3、mysql乐观锁
4、消息中间件

redis解决方案

redis命令

SETNX

SETNX key val
当且仅当key不存在时,set一个key为val的字符串,返回1;若key存在,则什么都不做,返回0。

expire

expire key timeout
为key设置一个超时时间,单位为second,超过这个时间锁会自动释放,避免死锁。

delete

delete key
删除key

实现方式

  1. bug示例:

    Jedis client = jedisPool.getResource();
    //当前锁空闲
     if(client.setnx("lockResource",value) == 0){
         //设置锁时间 
         client.expire("",expire);
     }
    

    存在问题解析:

    在对锁的操作不具有原子性,(加锁和时间设置)。当出现进程在设置时间时候崩溃错误问题,锁将永远保存,不主动处理。

  2. 不推荐示例,将过期+当前时间戳时间设置为value,通过比对来设置锁

    public boolean lock(String key, long expire, long acquireTimeout) {
        Jedis conn = null;
        String retIdentifier = null;
        try {
            // 获取连接
            conn = jedisPool.getResource();
            long end = System.currentTimeMillis() + acquireTimeout;
            while (System.currentTimeMillis() < end) {
                //锁的时间
                long lockExpire = System.currentTimeMillis() + expire;
                if (conn.setnx(key, String.valueOf(lockExpire)) == 1) {
                    return true;
                }
                String currentLockExpire = conn.get(key);
                //锁过期
                if (lockExpire < System.currentTimeMillis()) {
                    //获取上一个所的时间并设置新时间
                    String oldLockExpire = conn.getSet(key, String.valueOf(lockExpire));
                    if (currentLockExpire.equals(oldLockExpire)) {
                        return true;
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }
    

    存在问题解析:

    • 需要分布式下每个客户端的时间保持一致;
    • 锁快过期时,多个客户端同时执行getSet,虽然最终只有一个客户端可以加锁,但该客户端锁的过期时间可能被其他客户端覆盖;
    • 不具备拥有者标识,任何客户端都可以解锁。
  3. 最终解决方案

    Jedis conn = jedisPool.getResource();
    // SET IF NOT EXIST,而且还是原子的操作成功,返回“OK”,否则返回null
    conn.set(key, value, "NX", "EX", expireSeconds); 
    
  4. 解锁

    • del:用于删除已存在的键
    • pttl:以毫秒为单位返回key的剩余过期时间

    bug示例

    public boolean unlock(String key){
        if(1==conn.redis.del(key)){
           return ture; 
        }
        return false;
    }
    

    错误分析:

    ​ 不具备拥有者标识,谁都可以删掉。

    进阶修改:增加一个返回value如果一致则删除

    public boolean unlockPlus(String key,String localValue){
        if(localValue=conn.get(key)){
            //存在的问题是:此时key过期了被其他进程刚锁定,就有可能其他进程的锁被删掉。
            if(1==conn.redis.del(key)){
          		return ture; 
        	}
        }
        return false;
    }
    

    错误分析:

    ​ 不具备原则性

    最终版本:

    
    public boolean compareAndDel(final String key, final String value) {
        return execute(new JedisAction<Boolean>() {
            String lua = "   local val = redis.call('get', KEYS[1])\n" +
                "             if val == ARGV[1] then\n" +
                "             redis.call('del', KEYS[1])\n" +
                "             return true\n" +
                "             end";
            /**
            lua 脚本保证一系列操作的原子性
             local val = redis.call('get', KEYS[1])
             if val == ARGV[1] then
             redis.call('del', KEYS[1])
             return true
             end
             */
            @Override
            public Boolean action(Jedis jedis) {
                conn.evalsha(jedis.scriptLoad(lua),
                             Lists.newArrayList(key),Lists.newArrayList(value));
                return true;
            }
        });
    }
    

    解释:

    通过redis里eval命令操作lua代码,这样可以确保在解锁时保持原子性,而不会因为进程的崩溃导致解锁失败

锁到期

那么对应到锁的应用上也是这样,当占有锁的时间快到了但是此时业务未处理完,可以延长锁的过期时间,即锁支持可重入。

节点挂掉

当主节点还没有数据同步就挂掉:当在集群上实现分布式锁的时候,master节点宕机且数据未同步至slave节点时,此时就会出现多个客户端拥有一把锁的情况,违背分布式锁机制互斥性。

解决:
多个主节点冗余,核心思想是同时使用多个Redis Master来冗余,且这些节点是完全独立的,也不需要对这些节点之间的数据进行同步。获取集群中多数master节点上的锁,同时全部获取,否则全部释放。

参考:公众号:IT界农民工

posted @ 2021-03-19 19:26  墨水梦想  阅读(61)  评论(0编辑  收藏  举报