您的位置:首页 > 数据库 > Redis

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

2018-09-10 16:48 513 查看

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 工程

参考:

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: