zoukankan      html  css  js  c++  java
  • 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])
      " +
                  "             if val == ARGV[1] then
      " +
                  "             redis.call('del', KEYS[1])
      " +
                  "             return true
      " +
                  "             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界农民工

  • 相关阅读:
    WSP部署错误—SharePoint管理框架中的对象“SPSolutionLanguagePack Name=0”依赖其他不存在的对象
    Elevate Permissions To Modify User Profile
    Error with Stsadm CommandObject reference not set to an instance of an object
    ASP.NET MVC3添加Controller时没有Scaffolding options
    测试使用Windows Live Writer写日志
    配置TFS 2010出现错误—SQL Server 登录的安全标识符(SID)与某个指定的域或工作组帐户冲突
    使用ADO.NET DbContext Generator出现错误—Unable to locate file
    CSS
    HTML DIV标签
    数据库
  • 原文地址:https://www.cnblogs.com/JunQiang-Ma/p/14559155.html
Copyright © 2011-2022 走看看