• 基于redis 实现分布式锁(二)


    https://blog.csdn.net/xiaolyuh123/article/details/78551345

    分布式锁的解决方式

    1. 基于数据库表做乐观锁,用于分布式锁。(适用于小并发)
    2. 使用memcached的add()方法,用于分布式锁。
    3. 使用memcached的cas()方法,用于分布式锁。(不常用)
    4. 使用redis的setnx()、expire()方法,用于分布式锁。
    5. 使用redis的setnx()、get()、getset()方法,用于分布式锁。
    6. 使用redis的watch、multi、exec命令,用于分布式锁。(不常用)
    7. 使用zookeeper,用于分布式锁。(不常用)

    这里主要介绍第四种和第五种:

    前文提供的两种方式其实都有些问题,要么是死锁,要么是依赖服务器时间同步。从Redis 2.6.12 版本开始, SET 命令可以通过参数来实现和 SETNX 、 SETEX 和 PSETEX 三个命令的效果。这样我们的可以将加锁操作用一个set命令来实现,直接是原子性操作,既没有死锁的风险,也不依赖服务器时间同步,可以完美解决这两个问题。
    在redis文档上有详细说明:
    http://doc.redisfans.com/string/set.html

    使用redis的SET resource-name anystring NX EX max-lock-time 方式,用于分布式锁

    原理

    命令 SET resource-name anystring NX EX max-lock-time 是一种在 Redis 中实现锁的简单方法。

    客户端执行以上的命令:

    • 如果服务器返回 OK ,那么这个客户端获得锁。
    • 如果服务器返回 NIL ,那么客户端获取锁失败,可以在稍后再重试。
    • 设置的过期时间到达之后,锁将自动释放。

    可以通过以下修改,让这个锁实现更健壮:

    • 不使用固定的字符串作为键的值,而是设置一个不可猜测(non-guessable)的长随机字符串,作为口令串(token)。
    • 不使用 DEL 命令来释放锁,而是发送一个 Lua 脚本,这个脚本只在客户端传入的值和键的口令串相匹配时,才对键进行删除。
      这两个改动可以防止持有过期锁的客户端误删现有锁的情况出现。

    以下是一个简单的解锁脚本示例:

    1.  
      if redis.call("get",KEYS[1]) == ARGV[1]
    2.  
      then
    3.  
      return redis.call("del",KEYS[1])
    4.  
      else
    5.  
      return 0
    6.  
      end

    可能存在的问题

    占时没发现

    具体实现

    锁具体实现RedisLock:

    1.  
      package com.xiaolyuh.lock;
    2.  
       
    3.  
      import org.slf4j.Logger;
    4.  
      import org.slf4j.LoggerFactory;
    5.  
      import org.springframework.dao.DataAccessException;
    6.  
      import org.springframework.data.redis.connection.RedisConnection;
    7.  
      import org.springframework.data.redis.core.RedisCallback;
    8.  
      import org.springframework.data.redis.core.StringRedisTemplate;
    9.  
      import org.springframework.data.redis.core.script.RedisScript;
    10.  
      import org.springframework.util.Assert;
    11.  
      import org.springframework.util.StringUtils;
    12.  
      import redis.clients.jedis.Jedis;
    13.  
      import redis.clients.jedis.JedisCluster;
    14.  
      import redis.clients.jedis.Protocol;
    15.  
      import redis.clients.util.SafeEncoder;
    16.  
       
    17.  
      import java.util.ArrayList;
    18.  
      import java.util.List;
    19.  
      import java.util.Random;
    20.  
      import java.util.UUID;
    21.  
       
    22.  
      /**
    23.  
      * Redis分布式锁
    24.  
      * 使用 SET resource-name anystring NX EX max-lock-time 实现
    25.  
      * <p>
    26.  
      * 该方案在 Redis 官方 SET 命令页有详细介绍。
    27.  
      * http://doc.redisfans.com/string/set.html
    28.  
      * <p>
    29.  
      * 在介绍该分布式锁设计之前,我们先来看一下在从 Redis 2.6.12 开始 SET 提供的新特性,
    30.  
      * 命令 SET key value [EX seconds] [PX milliseconds] [NX|XX],其中:
    31.  
      * <p>
    32.  
      * EX seconds — 以秒为单位设置 key 的过期时间;
    33.  
      * PX milliseconds — 以毫秒为单位设置 key 的过期时间;
    34.  
      * NX — 将key 的值设为value ,当且仅当key 不存在,等效于 SETNX。
    35.  
      * XX — 将key 的值设为value ,当且仅当key 存在,等效于 SETEX。
    36.  
      * <p>
    37.  
      * 命令 SET resource-name anystring NX EX max-lock-time 是一种在 Redis 中实现锁的简单方法。
    38.  
      * <p>
    39.  
      * 客户端执行以上的命令:
    40.  
      * <p>
    41.  
      * 如果服务器返回 OK ,那么这个客户端获得锁。
    42.  
      * 如果服务器返回 NIL ,那么客户端获取锁失败,可以在稍后再重试。
    43.  
      *
    44.  
      * @author yuhao.wangwang
    45.  
      * @version 1.0
    46.  
      * @date 2017年11月3日 上午10:21:27
    47.  
      */
    48.  
      public class RedisLock3 {
    49.  
       
    50.  
      private static Logger logger = LoggerFactory.getLogger(RedisLock3.class);
    51.  
       
    52.  
      private StringRedisTemplate redisTemplate;
    53.  
       
    54.  
      /**
    55.  
      * 将key 的值设为value ,当且仅当key 不存在,等效于 SETNX。
    56.  
      */
    57.  
      public static final String NX = "NX";
    58.  
       
    59.  
      /**
    60.  
      * seconds — 以秒为单位设置 key 的过期时间,等效于EXPIRE key seconds
    61.  
      */
    62.  
      public static final String EX = "EX";
    63.  
       
    64.  
      /**
    65.  
      * 调用set后的返回值
    66.  
      */
    67.  
      public static final String OK = "OK";
    68.  
       
    69.  
      /**
    70.  
      * 默认请求锁的超时时间(ms 毫秒)
    71.  
      */
    72.  
      private static final long TIME_OUT = 100;
    73.  
       
    74.  
      /**
    75.  
      * 默认锁的有效时间(s)
    76.  
      */
    77.  
      public static final int EXPIRE = 60;
    78.  
       
    79.  
      /**
    80.  
      * 解锁的lua脚本
    81.  
      */
    82.  
      public static final String UNLOCK_LUA;
    83.  
       
    84.  
      static {
    85.  
      StringBuilder sb = new StringBuilder();
    86.  
      sb.append("if redis.call("get",KEYS[1]) == ARGV[1] ");
    87.  
      sb.append("then ");
    88.  
      sb.append(" return redis.call("del",KEYS[1]) ");
    89.  
      sb.append("else ");
    90.  
      sb.append(" return 0 ");
    91.  
      sb.append("end ");
    92.  
      UNLOCK_LUA = sb.toString();
    93.  
      }
    94.  
       
    95.  
      /**
    96.  
      * 锁标志对应的key
    97.  
      */
    98.  
      private String lockKey;
    99.  
       
    100.  
      /**
    101.  
      * 记录到日志的锁标志对应的key
    102.  
      */
    103.  
      private String lockKeyLog = "";
    104.  
       
    105.  
      /**
    106.  
      * 锁对应的值
    107.  
      */
    108.  
      private String lockValue;
    109.  
       
    110.  
      /**
    111.  
      * 锁的有效时间(s)
    112.  
      */
    113.  
      private int expireTime = EXPIRE;
    114.  
       
    115.  
      /**
    116.  
      * 请求锁的超时时间(ms)
    117.  
      */
    118.  
      private long timeOut = TIME_OUT;
    119.  
       
    120.  
      /**
    121.  
      * 锁标记
    122.  
      */
    123.  
      private volatile boolean locked = false;
    124.  
       
    125.  
      final Random random = new Random();
    126.  
       
    127.  
      /**
    128.  
      * 使用默认的锁过期时间和请求锁的超时时间
    129.  
      *
    130.  
      * @param redisTemplate
    131.  
      * @param lockKey 锁的key(Redis的Key)
    132.  
      */
    133.  
      public RedisLock3(StringRedisTemplate redisTemplate, String lockKey) {
    134.  
      this.redisTemplate = redisTemplate;
    135.  
      this.lockKey = lockKey + "_lock";
    136.  
      }
    137.  
       
    138.  
      /**
    139.  
      * 使用默认的请求锁的超时时间,指定锁的过期时间
    140.  
      *
    141.  
      * @param redisTemplate
    142.  
      * @param lockKey 锁的key(Redis的Key)
    143.  
      * @param expireTime 锁的过期时间(单位:秒)
    144.  
      */
    145.  
      public RedisLock3(StringRedisTemplate redisTemplate, String lockKey, int expireTime) {
    146.  
      this(redisTemplate, lockKey);
    147.  
      this.expireTime = expireTime;
    148.  
      }
    149.  
       
    150.  
      /**
    151.  
      * 使用默认的锁的过期时间,指定请求锁的超时时间
    152.  
      *
    153.  
      * @param redisTemplate
    154.  
      * @param lockKey 锁的key(Redis的Key)
    155.  
      * @param timeOut 请求锁的超时时间(单位:毫秒)
    156.  
      */
    157.  
      public RedisLock3(StringRedisTemplate redisTemplate, String lockKey, long timeOut) {
    158.  
      this(redisTemplate, lockKey);
    159.  
      this.timeOut = timeOut;
    160.  
      }
    161.  
       
    162.  
      /**
    163.  
      * 锁的过期时间和请求锁的超时时间都是用指定的值
    164.  
      *
    165.  
      * @param redisTemplate
    166.  
      * @param lockKey 锁的key(Redis的Key)
    167.  
      * @param expireTime 锁的过期时间(单位:秒)
    168.  
      * @param timeOut 请求锁的超时时间(单位:毫秒)
    169.  
      */
    170.  
      public RedisLock3(StringRedisTemplate redisTemplate, String lockKey, int expireTime, long timeOut) {
    171.  
      this(redisTemplate, lockKey, expireTime);
    172.  
      this.timeOut = timeOut;
    173.  
      }
    174.  
       
    175.  
      /**
    176.  
      * 尝试获取锁 超时返回
    177.  
      *
    178.  
      * @return
    179.  
      */
    180.  
      public boolean tryLock() {
    181.  
      // 生成随机key
    182.  
      lockValue = UUID.randomUUID().toString();
    183.  
      // 请求锁超时时间,纳秒
    184.  
      long timeout = timeOut * 1000000;
    185.  
      // 系统当前时间,纳秒
    186.  
      long nowTime = System.nanoTime();
    187.  
      while ((System.nanoTime() - nowTime) < timeout) {
    188.  
      if (OK.equalsIgnoreCase(this.set(lockKey, lockValue, expireTime))) {
    189.  
      locked = true;
    190.  
      // 上锁成功结束请求
    191.  
      return true;
    192.  
      }
    193.  
       
    194.  
      // 每次请求等待一段时间
    195.  
      seleep(10, 50000);
    196.  
      }
    197.  
      return locked;
    198.  
      }
    199.  
       
    200.  
      /**
    201.  
      * 尝试获取锁 立即返回
    202.  
      *
    203.  
      * @return 是否成功获得锁
    204.  
      */
    205.  
      public boolean lock() {
    206.  
      lockValue = UUID.randomUUID().toString();
    207.  
      //不存在则添加 且设置过期时间(单位ms)
    208.  
      String result = set(lockKey, lockValue, expireTime);
    209.  
      return OK.equalsIgnoreCase(result);
    210.  
      }
    211.  
       
    212.  
      /**
    213.  
      * 以阻塞方式的获取锁
    214.  
      *
    215.  
      * @return 是否成功获得锁
    216.  
      */
    217.  
      public boolean lockBlock() {
    218.  
      lockValue = UUID.randomUUID().toString();
    219.  
      while (true) {
    220.  
      //不存在则添加 且设置过期时间(单位ms)
    221.  
      String result = set(lockKey, lockValue, expireTime);
    222.  
      if (OK.equalsIgnoreCase(result)) {
    223.  
      return true;
    224.  
      }
    225.  
       
    226.  
      // 每次请求等待一段时间
    227.  
      seleep(10, 50000);
    228.  
      }
    229.  
      }
    230.  
       
    231.  
      /**
    232.  
      * 解锁
    233.  
      * <p>
    234.  
      * 可以通过以下修改,让这个锁实现更健壮:
    235.  
      * <p>
    236.  
      * 不使用固定的字符串作为键的值,而是设置一个不可猜测(non-guessable)的长随机字符串,作为口令串(token)。
    237.  
      * 不使用 DEL 命令来释放锁,而是发送一个 Lua 脚本,这个脚本只在客户端传入的值和键的口令串相匹配时,才对键进行删除。
    238.  
      * 这两个改动可以防止持有过期锁的客户端误删现有锁的情况出现。
    239.  
      */
    240.  
      public Boolean unlock() {
    241.  
      // 只有加锁成功并且锁还有效才去释放锁
    242.  
      // 只有加锁成功并且锁还有效才去释放锁
    243.  
      if (locked) {
    244.  
      return redisTemplate.execute(new RedisCallback<Boolean>() {
    245.  
      @Override
    246.  
      public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
    247.  
      Object nativeConnection = connection.getNativeConnection();
    248.  
      Long result = 0L;
    249.  
       
    250.  
      List<String> keys = new ArrayList<>();
    251.  
      keys.add(lockKey);
    252.  
      List<String> values = new ArrayList<>();
    253.  
      values.add(lockValue);
    254.  
       
    255.  
      // 集群模式
    256.  
      if (nativeConnection instanceof JedisCluster) {
    257.  
      result = (Long) ((JedisCluster) nativeConnection).eval(UNLOCK_LUA, keys, values);
    258.  
      }
    259.  
       
    260.  
      // 单机模式
    261.  
      if (nativeConnection instanceof Jedis) {
    262.  
      result = (Long) ((Jedis) nativeConnection).eval(UNLOCK_LUA, keys, values);
    263.  
      }
    264.  
       
    265.  
      if (result == 0 && !StringUtils.isEmpty(lockKeyLog)) {
    266.  
      logger.info("Redis分布式锁,解锁{}失败!解锁时间:{}", lockKeyLog, System.currentTimeMillis());
    267.  
      }
    268.  
       
    269.  
      locked = result == 0;
    270.  
      return result == 1;
    271.  
      }
    272.  
      });
    273.  
      }
    274.  
       
    275.  
      return true;
    276.  
      }
    277.  
       
    278.  
      /**
    279.  
      * 重写redisTemplate的set方法
    280.  
      * <p>
    281.  
      * 命令 SET resource-name anystring NX EX max-lock-time 是一种在 Redis 中实现锁的简单方法。
    282.  
      * <p>
    283.  
      * 客户端执行以上的命令:
    284.  
      * <p>
    285.  
      * 如果服务器返回 OK ,那么这个客户端获得锁。
    286.  
      * 如果服务器返回 NIL ,那么客户端获取锁失败,可以在稍后再重试。
    287.  
      *
    288.  
      * @param key 锁的Key
    289.  
      * @param value 锁里面的值
    290.  
      * @param seconds 过去时间(秒)
    291.  
      * @return
    292.  
      */
    293.  
      private String set(final String key, final String value, final long seconds) {
    294.  
      Assert.isTrue(!StringUtils.isEmpty(key), "key不能为空");
    295.  
      return redisTemplate.execute(new RedisCallback<String>() {
    296.  
      @Override
    297.  
      public String doInRedis(RedisConnection connection) throws DataAccessException {
    298.  
      Object nativeConnection = connection.getNativeConnection();
    299.  
      String result = null;
    300.  
      // 集群模式
    301.  
      if (nativeConnection instanceof JedisCluster) {
    302.  
      result = ((JedisCluster) nativeConnection).set(key, value, NX, EX, seconds);
    303.  
      }
    304.  
      // 单机模式
    305.  
      if (nativeConnection instanceof Jedis) {
    306.  
      result = ((Jedis) nativeConnection).set(key, value, NX, EX, seconds);
    307.  
      }
    308.  
       
    309.  
      if (!StringUtils.isEmpty(lockKeyLog) && !StringUtils.isEmpty(result)) {
    310.  
      logger.info("获取锁{}的时间:{}", lockKeyLog, System.currentTimeMillis());
    311.  
      }
    312.  
       
    313.  
      return result;
    314.  
      }
    315.  
      });
    316.  
      }
    317.  
       
    318.  
      /**
    319.  
      * @param millis 毫秒
    320.  
      * @param nanos 纳秒
    321.  
      * @Title: seleep
    322.  
      * @Description: 线程等待时间
    323.  
      * @author yuhao.wang
    324.  
      */
    325.  
      private void seleep(long millis, int nanos) {
    326.  
      try {
    327.  
      Thread.sleep(millis, random.nextInt(nanos));
    328.  
      } catch (InterruptedException e) {
    329.  
      logger.info("获取分布式锁休眠被中断:", e);
    330.  
      }
    331.  
      }
    332.  
       
    333.  
      public String getLockKeyLog() {
    334.  
      return lockKeyLog;
    335.  
      }
    336.  
       
    337.  
      public void setLockKeyLog(String lockKeyLog) {
    338.  
      this.lockKeyLog = lockKeyLog;
    339.  
      }
    340.  
       
    341.  
      public int getExpireTime() {
    342.  
      return expireTime;
    343.  
      }
    344.  
       
    345.  
      public void setExpireTime(int expireTime) {
    346.  
      this.expireTime = expireTime;
    347.  
      }
    348.  
       
    349.  
      public long getTimeOut() {
    350.  
      return timeOut;
    351.  
      }
    352.  
       
    353.  
      public void setTimeOut(long timeOut) {
    354.  
      this.timeOut = timeOut;
    355.  
      }
    356.  
      }
    357.  
       

    调用方式:

    1.  
      public void redisLock3(int i) {
    2.  
      RedisLock3 redisLock3 = new RedisLock3(redisTemplate, "redisLock:" + i % 10, 5 * 60, 500);
    3.  
      try {
    4.  
      long now = System.currentTimeMillis();
    5.  
      if (redisLock3.tryLock()) {
    6.  
      logger.info("=" + (System.currentTimeMillis() - now));
    7.  
      // TODO 获取到锁要执行的代码块
    8.  
      logger.info("j:" + j++);
    9.  
      } else {
    10.  
      logger.info("k:" + k++);
    11.  
      }
    12.  
      } catch (Exception e) {
    13.  
      logger.info(e.getMessage(), e);
    14.  
      } finally {
    15.  
      redisLock2.unlock();
    16.  
      }
    17.  
      }
    18.  
       

    对于这种种redis实现分布式锁的方案还是有一个问题:就是你获取锁后执行业务逻辑的代码只能在redis锁的有效时间之内,因为,redis的key到期后会自动清除,这个锁就算释放了。所以这个锁的有效时间一定要结合业务做好评估。

    源码: https://github.com/wyh-spring-ecosystem-student/spring-boot-student/tree/releases

    spring-boot-student-data-redis-distributed-lock 工程

    参考:

  • 相关阅读:
    JS内容左右滑动
    JS返回上一页
    两栏 三栏的css
    舅舅去世
    .net学习开始
    以论坛管理的方式来处理公司资讯
    《尽管去做》摘
    网页视频播放器代码集
    火影忍者和海贼王
    古代风水文献
  • 原文地址:https://www.cnblogs.com/ruiati/p/9620847.html
Copyright © 2020-2023  润新知