1 redis分布式加锁

现在业务并发量越来越大,像传统的数据库操作,已经不能满足要求了,这个时候可以使用redis来提升性能,同时也可以使用redis实现分布式锁。

使用redis实现分布式锁,与java的synchronize类似,只不过是synchronize锁单对象,而分布式锁是锁进程或者线程,同样的它是一个独占锁,一旦被某个线程拿到锁,其他的线程或者进程,只能进行等待后再获取。当线程或者进程使用完毕释放锁后,其他线程才能获取到锁。

下面将基于redis实现一个分布式锁。

代码实现

先看Redis的分布式加锁的实现.

public class RedisLock {

  /** 加锁的key */
  private static final String LOCK_KEY = "lock.key";

  /** 超时时间 */
  private static final int TIME_OUT = 1000 * 60;

  /** 执行解锁的命令 */
  private static final String UNLOCK_COMMAND =
      "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

  /**
   * 分布式加锁操作
   *
   * @param dataId 用于标识唯一的id
   * @return 加锁的结果
   */
  public boolean lock(String dataId) {
    // nx不存在时进行
    // px超时时间
    SetParams param = SetParams.setParams().nx().px(TIME_OUT);

    Jedis connect = RedisConn.getRedisConn();
    String result;
    try {
      result = connect.set(LOCK_KEY, dataId, param);
    } finally {
      if (connect != null) {
        connect.close();
      }
    }

    System.out.println(
        "  lock thread id :"
            + Thread.currentThread().getId()
            + ",rsp :"
            + result
            + ",compare rsp : "
            + (RedisConn.SUCCESS.equals(result)));
    // 检查结果是否设置成功
    return RedisConn.SUCCESS.equals(result);
  }

  AtomicInteger num = new AtomicInteger(0);

  /**
   * 执行解锁操作
   *
   * @return true 解锁成功 false 解锁失败
   */
  public boolean unlock(String dataId) {
    Jedis connect = RedisConn.getRedisConn();
    Object result;
    try {
      result = connect.eval(UNLOCK_COMMAND, 1, LOCK_KEY, dataId);
    } finally {
      if (connect != null) {
        connect.close();
      }
    }

    System.out.println(
        "  unlock thread id :"
            + Thread.currentThread().getId()
            + ",rsp :"
            + result
            + ",compare rsp : "
            + (RedisConn.RELEASE_SUCCESS.equals(result))
            + "run Num "
            + num.incrementAndGet());

    // Lua代码将被当成一个命令去执行,并且直到eval命令执行完成,Redis才会执行其他命令。
    return RedisConn.RELEASE_SUCCESS.equals(result);
  }
}

先说加锁吧:

在redis中加锁的命令是set,设置参数nx表示不存在时创建,px设置加锁的超时时间,这样加锁命令才是原子操作。

注:在很多实现版本中使用的是setnx加expire组合,这个组存着一个问题,那就是客户端执行setnx后宕机了,超时时间是没有被设置的,极端情况将导致分布式锁不可用。

再来说解锁操作吧:

解锁使用是redsi执行lua脚本的功能,来完成解锁的功能。解锁操作的流程是判断当前加锁的id与解锁的id是否一致,如果一致,则可以执行解锁命令del操作。如果不一致,则直接返回。那为什么要使用lua执行命令,而不是使用命令行判断再删除呢?

这两种操作表面看起来达到的效果是一致的。都是先检查对比id是否一致,再执行删除操作。 这是因为检查与删除是两个命令操作,在某些情况下,如果存储的id一致,将导致ABA问题,意外的释放了锁。

Redis 使用单个 Lua 解释器去运行所有脚本,并且 Redis 也保证脚本会以原子性(atomic)的方式执行:当某个脚本正在运行的时候,不会有其他脚本或 Redis 命令被执行。

redis的连接处理.

public class RedisConn {

  /** 成功的标识 */
  public static final String SUCCESS = "OK";

  /** 命令操作操作成功 */
  private static final int OPERATOR_OK = 1;

  /** 执行操作成功的标识 */
  public static final Long RELEASE_SUCCESS = 1L;

  /** 密码 */
  private static final String PASSWORD = "123456";

  private static final JedisPoolConfig CONFIG = new JedisPoolConfig();

  /** 连接信息 */
  private static final JedisPool POOL = new JedisPool(CONFIG, "localhost");

  /**
   * 获取连接
   *
   * @return
   */
  public static Jedis getRedisConn() {
    Jedis jedis = POOL.getResource();
    // 密码操作
    String authRsp = jedis.auth(PASSWORD);

    if (SUCCESS.equals(authRsp)) {
      return jedis;
    }

    throw new IllegalArgumentException("password error");
  }
}

商品

public class Goods {

  /** 商品名称 */
  private String name;

  /** 商品数量操作的key */
  private static final String GOODS_NUM = "GOODS.NUM";

  /** 使用redis进行分布式锁操作 */
  private RedisLock lock;

  public Goods(String name, int goodsNum, RedisLock lock) {
    this.name = name;
    // 设置商品数量
    this.updateGoodsNum(goodsNum);
    this.lock = lock;
  }

  /** 商品的库存扣减操作 */
  public int minusGoods(int num) {
    String dataId = UUID.randomUUID().toString();

    int rsp = 0;
    boolean lockRsp;
    do {
      lockRsp = lock.lock(dataId);
      try {
        // 加锁成功进行业务操作
        if (lockRsp) {
          // 获取当前的数量
          int getGoods = this.getGoodsNum();
          // 执行库存的扣除操作
          if (getGoods >= num) {
            this.updateGoodsNum(getGoods - num);
            rsp = num;
          }
        }
      } finally {
        // 仅当加锁成功时才执行解锁操作
        if (lockRsp) {
          lock.unlock(dataId);
        }
      }

      // 当加锁失败时,则继续尝试,不要一直轮训,休息个随机时间,2毫秒,到10毫秒之间
      if (!lockRsp) {
        int randSleep = ThreadLocalRandom.current().nextInt(2, 10);
        currSleep(randSleep);
      }
      // 当加锁失败时继续
    } while (!lockRsp);
    return rsp;
  }

  /**
   * 进行设置的库最最新修改操作
   *
   * @param nums 最新的商品数量
   */
  private void updateGoodsNum(int nums) {
    Jedis jedis = RedisConn.getRedisConn();
    try {
      jedis.set(GOODS_NUM, String.valueOf(nums));
    } finally {
      jedis.close();
    }
  }

  /**
   * 获取当前最新的数量
   *
   * @return
   */
  private int getGoodsNum() {
    Jedis jedis = RedisConn.getRedisConn();
    try {
      String value = jedis.get(GOODS_NUM);
      if (StringUtils.isNotEmpty(value)) {
        return Integer.parseInt(value);
      }
    } finally {
      jedis.close();
    }

    return 0;
  }

  /**
   * 休眠的时间
   *
   * @param sleepTIme
   */
  private void currSleep(long sleepTIme) {
    try {
      Thread.sleep(sleepTIme);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }

  /**
   * 获取商品数量
   *
   * @return 当前商品的数量
   */
  public int getGoods() {
    return this.getGoodsNum();
  }
}

订单:

public class Orders {

  /** 商品服务 */
  private Goods goods;

  public Orders(Goods goods) {
    this.goods = goods;
  }

  /**
   * 创建订单
   *
   * @return
   */
  public boolean createOrder(int num) {
    // 执行扣减库存操作
    int rsp = goods.minusGoods(num);
    return rsp > 0;
  }
}

单元测试:

public class TestRedisLock {
  @Test
  public void useOrder() throws InterruptedException {
    int orderNumSum = 800;
    RedisLock redisLock = new RedisLock();
    Goods goods = new Goods("mac", orderNumSum, redisLock);

    // 并发进行下单操作
    int maxOrder = 4;

    int count = 0;
    for (int i = 0; i < orderNumSum / maxOrder; i++) {
      CountDownLatch startLatch = new CountDownLatch(maxOrder);
      for (int j = 0; j < maxOrder; j++) {
        TaskThreadPool.INSTANCE.submit(
            () -> {
              startLatch.countDown();

              Orders instance = new Orders(goods);
              instance.createOrder(1);
            });

        count++;
      }
      // 执行等待结果
      try {
        startLatch.await();
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
    System.out.println("结束,共运行:" + count + "次");

    TaskThreadPool.INSTANCE.shutdown();

    Thread.sleep(500);

    System.out.println("shutdown status:" + TaskThreadPool.INSTANCE.getPool().isShutdown());
  }
}

最后查看控制台的输出

lock thread id :15,rsp :OK,compare rsp : true
  lock thread id :18,rsp :null,compare rsp : false
  lock thread id :20,rsp :null,compare rsp : false
  lock thread id :19,rsp :null,compare rsp : false
  lock thread id :16,rsp :null,compare rsp : false
  lock thread id :17,rsp :null,compare rsp : false
  lock thread id :21,rsp :null,compare rsp : false
  lock thread id :14,rsp :null,compare rsp : false
  unlock thread id :15,rsp :1,compare rsp : truerun Num 1
  lock thread id :15,rsp :OK,compare rsp : true
  unlock thread id :15,rsp :1,compare rsp : truerun Num 2
  lock thread id :15,rsp :OK,compare rsp : true
  unlock thread id :15,rsp :1,compare rsp : truerun Num 3
  lock thread id :15,rsp :OK,compare rsp : true
  unlock thread id :15,rsp :1,compare rsp : truerun Num 4
  lock thread id :14,rsp :OK,compare rsp : true
  lock thread id :19,rsp :null,compare rsp : false
  lock thread id :15,rsp :null,compare rsp : false
  unlock thread id :14,rsp :1,compare rsp : truerun Num 5
  lock thread id :14,rsp :OK,compare rsp : true
  lock thread id :18,rsp :null,compare rsp : false
  unlock thread id :14,rsp :1,compare rsp : truerun Num 6
  lock thread id :14,rsp :OK,compare rsp : true
  ......
  lock thread id :18,rsp :null,compare rsp : false
  unlock thread id :21,rsp :1,compare rsp : truerun Num 792
  结束,共运行:800次
  lock thread id :21,rsp :OK,compare rsp : true
  unlock thread id :21,rsp :1,compare rsp : truerun Num 793
  lock thread id :15,rsp :OK,compare rsp : true
  unlock thread id :15,rsp :1,compare rsp : truerun Num 794
  lock thread id :20,rsp :OK,compare rsp : true
  lock thread id :14,rsp :null,compare rsp : false
  unlock thread id :20,rsp :1,compare rsp : truerun Num 795
  lock thread id :17,rsp :OK,compare rsp : true
  unlock thread id :17,rsp :1,compare rsp : truerun Num 796
  lock thread id :16,rsp :OK,compare rsp : true
  unlock thread id :16,rsp :1,compare rsp : truerun Num 797
  lock thread id :18,rsp :OK,compare rsp : true
  unlock thread id :18,rsp :1,compare rsp : truerun Num 798
  lock thread id :14,rsp :OK,compare rsp : true
  lock thread id :19,rsp :null,compare rsp : false
  unlock thread id :14,rsp :1,compare rsp : truerun Num 799
  lock thread id :19,rsp :OK,compare rsp : true
  unlock thread id :19,rsp :1,compare rsp : truerun Num 800
shutdown status:true

最后检查下redis中是否正确的处理:

D:\java\soft\redis\Redis-x64-5.0.10>redis-cli.exe
127.0.0.1:6379> auth 123456
OK
127.0.0.1:6379> keys *
1) "GOODS.NUM"
127.0.0.1:6379> get GOODS.NUM
"0"
127.0.0.1:6379>

从结果中可以看到,商品的数量已经正确的扣减到了0。

劣势

锁超时时间的设置,但这个超时时间并没有一个统一的设置,需要根据业务的情况设置锁超时的时间,但是由于业务的复杂性,导致超时时间的粒度不同,如何根据业务匹配一个合适的超时间时间是一个比较麻烦的事情。需要做大量的测试,最终给出一个合理的超时时间。

如果在业务执行过程中,意外的宕机,锁将不能正常的释放,需要加入其他机制来保证。

业务在操作过程中也需要特别的小心,需要在异常的情况下也能正确的释放锁。

引入了redis的组件,维护的成本是必然上升的。

适用场景

相对于数据库来说,redis的并发度已经提升相当大了,可达十几万的并发,提升相当的高。针对大量并发来说,redis是一个抗流量的利器,但由于redis的操作锁的复杂度。操作需要特别小心,尤其是超时间,需要针对业务做大量的测试,确实一个合适的超时时间,过大,导致资源浪费,过小,锁将出现我意外释放,所以需要终合考量。

2 使用redis的cas机制

代码实现

还是以代码来说话吧:

public class Goods {

  /** 商品名称 */
  private String name;

  /** 商品数量操作的key */
  private static final String GOODS_NUM = "GOODS.NUM";

  /** 执行5次的连续尝试 */
  private static final int DEFAULT_MAX_TRY = 5;

  /** redis的比较交换操作 */
  private static final String REDIS_CAS =
      "if redis.call('get', KEYS[1]) == ARGV[1] then  redis.call('set', KEYS[1], ARGV[2]) return 1 else return 0 end ";

  public Goods(String name, int goodsNum) {
    this.name = name;
    // 设置商品数量
    this.updateGoodsNum(goodsNum);
  }

  /** 商品的库存扣减操作 */
  public int minusGoods(int num) {
    int rsp = 0;
    int index = 0;
    boolean casRsp;
    do {
      int goodsNum = this.getGoodsNum();
      casRsp = this.compareAndSet(goodsNum, goodsNum - num);

      System.out.println(
          "当前线程:"
              + Thread.currentThread().getId()
              + ",商量数量:"
              + goodsNum
              + ",更新后:"
              + (goodsNum - num)
              + ",结果:"
              + casRsp);

      // 当库存更新成功时,则退出操作
      if (casRsp) {
        rsp = num;
        break;
      }
      // 当加锁失败时,则继续尝试,不要一直轮训,休息个随机时间,5毫秒,到10毫秒之间
      if (index > DEFAULT_MAX_TRY) {
        int randSleep = ThreadLocalRandom.current().nextInt(5, 10);
        currSleep(randSleep);
      }
      // 当加锁失败时继续
    } while (true);
    return rsp;
  }

  /**
   * 进行设置的库最最新修改操作
   *
   * @param num 最新的商品数量
   */
  private void updateGoodsNum(int num) {
    RedisConn.getRedisConn().set(GOODS_NUM, String.valueOf(num));
  }

  /**
   * 基于比较交换的思想进行数据的更新操作,可以理解为lock free
   *
   * @param before 之前的数据
   * @param after 待修改后的数据
   * @return true cas操作成功 false cas操作失败
   */
  private boolean compareAndSet(int before, int after) {
    List<String> keys = new ArrayList<>();
    List<String> values = new ArrayList<>();
    keys.add(GOODS_NUM);
    values.add(String.valueOf(before));
    values.add(String.valueOf(after));

    Jedis conn = RedisConn.getRedisConn();
    Long casRsp;
    try {
      casRsp = (Long) conn.eval(REDIS_CAS, keys, values);
    } finally {
      if (null != conn) {
        conn.close();
      }
    }

    return RedisConn.RELEASE_SUCCESS.equals(casRsp);
  }

  /**
   * 获取当前最新的商品数量
   *
   * @return 当前数量的值
   */
  private int getGoodsNum() {
    Jedis conn = RedisConn.getRedisConn();
    try {
      String value = conn.get(GOODS_NUM);
      if (StringUtils.isNotEmpty(value)) {
        return Integer.parseInt(value);
      }
    } finally {
      if (null != conn) {
        conn.close();
      }
    }

    return 0;
  }

  /**
   * 休眠的时间
   *
   * @param sleepTime 休眠的时间
   */
  private void currSleep(long sleepTime) {
    try {
      Thread.sleep(sleepTime);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }

  /**
   * 获取商品数量
   *
   * @return 当前商品的数量
   */
  public int getGoods() {
    return this.getGoodsNum();
  }
}

这是一个商品库操作的类,使用redis执行lua脚本的原子特性,将多个操作封装到一个lua脚本中,以保证执行的原子性。这样相对分布式锁来说,资源的消耗更低,将以前三个操作,直接合并为一个开销,这样就直接减少了60%的操作,直接一次就可完成对应的操作。再加上是无锁的,这样就无用担心锁的释放问题。也不会发生锁长时间释放不掉的问题。

订单:

public class Orders {

  /** 商品服务 */
  private Goods goods;

  public Orders(Goods goods) {
    this.goods = goods;
  }

  /**
   * 创建订单
   *
   * @return
   */
  public boolean createOrder(int num) {
    // 执行扣减库存操作
    int rsp = goods.minusGoods(num);
    return rsp > 0;
  }
}

redis连接管理类

public class RedisConn {

  /** 成功的标识 */
  public static final String SUCCESS = "OK";

  /** 命令操作操作成功 */
  private static final int OPERATOR_OK = 1;

  /** 执行操作成功的标识 */
  public static final Long RELEASE_SUCCESS = 1L;

  /** 密码 */
  private static final String PASSWORD = "123456";

  private static final JedisPoolConfig CONFIG = new JedisPoolConfig();

  /** 连接信息 */
  private static final JedisPool POOL = new JedisPool(CONFIG, "localhost");

  /**
   * 获取连接
   *
   * @return
   */
  public static Jedis getRedisConn() {
    Jedis jedis = POOL.getResource();
    // 密码操作
    String authRsp = jedis.auth(PASSWORD);

    if (SUCCESS.equals(authRsp)) {
      return jedis;
    }

    throw new IllegalArgumentException("password error");
  }
}

劣势:

由于使用cas机制后,redis只保证当前执行的一个原子性,但针对每次都需要执行成功的场景,就需要不断尝试,直到尝试成功,这样就会导致更多的资源的消耗,所以如果此CAS机制不建议使用到并发度高且每次都需要成功的的应用,这样所导致的资源消耗是加倍的,极端点会跑满CPU,导致不能正常的响应。如果每次都需要成功的并且并发度很高,还是使用锁吧。

由于CAS操作是无锁的,所以代码比有锁的代码更复杂,需要考滤的情况也更多。

适用场景:

相对于分布式锁的redis的方案来说,redis的CAS方案所能承受的并发度也更大,同等数量级的情况下,比分布式锁大约有20%的提升。

redis的CAS机制适用于并发度高,但允许接受结果失败的场景; 也适用于并发度不是特别高,但必须成功的场景。这种场景可使用重试策略来进行结果的保证。