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

参考:

上一篇:Java设计模式(4)原型模式(Prototype模式)


下一篇:ASP.NET动态加载Js代码到Head标签中(三种方法)