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

参考:

基于redis 实现分布式锁(二)的更多相关文章

  1. 基于redis的分布式锁二种应用场景

    “分布式锁”是用来解决分布式应用中“并发冲突”的一种常用手段,实现方式一般有基于zookeeper及基于redis二种.具体到业务场景中,我们要考虑二种情况: 一.抢不到锁的请求,允许丢弃(即:忽略) ...

  2. 基于 redis 的分布式锁实现 Distributed locks with Redis debug 排查错误

    小结: 1. 锁的实现方式,按照应用的实现架构,可能会有以下几种类型: 如果处理程序是单进程多线程的,在 python下,就可以使用 threading 模块的 Lock 对象来限制对共享变量的同步访 ...

  3. redis系列:基于redis的分布式锁

    一.介绍 这篇博文讲介绍如何一步步构建一个基于Redis的分布式锁.会从最原始的版本开始,然后根据问题进行调整,最后完成一个较为合理的分布式锁. 本篇文章会将分布式锁的实现分为两部分,一个是单机环境, ...

  4. python基于redis实现分布式锁

    阅读目录 什么事分布式锁 基于redis实现分布式锁 一.什么是分布式锁 我们在开发应用的时候,如果需要对某一个共享变量进行多线程同步访问的时候,可以使用我们学到的锁进行处理,并且可以完美的运行,毫无 ...

  5. 基于redis 实现分布式锁的方案

    在电商项目中,经常有秒杀这样的活动促销,在并发访问下,很容易出现上述问题.如果在库存操作上,加锁就可以避免库存卖超的问题.分布式锁使分布式系统之间同步访问共享资源的一种方式 基于redis实现分布式锁 ...

  6. 基于redis的分布式锁

    <?php /** * 基于redis的分布式锁 * * 参考开源代码: * http://nleach.com/post/31299575840/redis-mutex-in-php * * ...

  7. 基于Redis的分布式锁真的安全吗?

    说明: 我前段时间写了一篇用consul实现分布式锁,感觉理解的也不是很好,直到我看到了这2篇写分布式锁的讨论,真的是很佩服作者严谨的态度, 把这种分布式锁研究的这么透彻,作者这种技术态度真的值得我好 ...

  8. 基于 Redis 的分布式锁

    前言 分布式锁在分布式应用中应用广泛,想要搞懂一个新事物首先得了解它的由来,这样才能更加的理解甚至可以举一反三. 首先谈到分布式锁自然也就联想到分布式应用. 在我们将应用拆分为分布式应用之前的单机系统 ...

  9. 基于redis的分布式锁(转)

    基于redis的分布式锁 1 介绍 这篇博文讲介绍如何一步步构建一个基于Redis的分布式锁.会从最原始的版本开始,然后根据问题进行调整,最后完成一个较为合理的分布式锁. 本篇文章会将分布式锁的实现分 ...

  10. 基于redis的分布式锁实现

    1.分布式锁介绍 在计算机系统中,锁作为一种控制并发的机制无处不在. 单机环境下,操作系统能够在进程或线程之间通过本地的锁来控制并发程序的行为.而在如今的大型复杂系统中,通常采用的是分布式架构提供服务 ...

随机推荐

  1. SPLAY,LCT学习笔记(二)

    能够看到,上一篇的代码中有一段叫做find我没有提到,感觉起来也没有什么用,那么他的存在意义是什么呢? 接下来我们来填一下这个坑 回到我们的主题:NOI 2005维修数列 我们刚刚讨论了区间翻转的操作 ...

  2. Oracle学习笔记--第2章 oracle 数据库体系结构

    第2章 oracle 数据库体系结构 目录: ————————————— 2.1物理存储结构 2.1.1数据文件 2.2.2控制文件 2.1.3重做日志文件 2.1.4其他文件 2.2逻辑存储结构 2 ...

  3. 【C++ Primer 第11章】2. 关联容器操作

    练习答案 一.访问元素 关联容器额外类型别名  key_type 此容器类型的关键字类型 mapped_type 每个关键字关联的类型,只 适用于map mapped_type 对于set,与key_ ...

  4. mysql数据库备份 mysqldump

    一.--all-databases /application/mysql3307/bin/mysqldump -uroot -S /application/mysql3307/logs/mysql.s ...

  5. 【Java】 剑指offer(51)数组中的逆序对

    本文参考自<剑指offer>一书,代码采用Java语言. 更多:<剑指Offer>Java实现合集   题目 在数组中的两个数字如果前面一个数字大于后面的数字,则这两个数字组成 ...

  6. 又是毕业季I

    洛谷P1372 又是毕业季I 对于答案a,k*a是最接近n的,即a=n/k(下取整),所以直接输n/k即可. 我的方法是 二分查找 n/k真的没有想到唉. 我找的最大公约数,如果当前的mid对应的个数 ...

  7. 002.AnyCast技术浅析

    一      常见通信方式 1.1  UniCastAnyCast UniCast,即单播,指网络中一个节点与另一个节点之间需要建立一个单独的数据通道,从一个节点发出的信息只被一个节点收到,这种传送方 ...

  8. Spring框架学习07——基于传统代理类的AOP实现

    在Spring中默认使用JDK动态代理实现AOP编程,使用org.springframework.aop.framework.ProxyFactoryBean创建代理是Spring AOP 实现的最基 ...

  9. String、StringBuffer和StringBulder

    String:不可改变的Unicode字符序列. 池化思想,把需要共享的数据放在池中,用一个存储区域来存放一些公用资源以减少存储空间的开销. 在String类中,以字面值创建时,会到Java方法空间的 ...

  10. 运营商DNS系统安全解决方案

    DNS系统面临的主要风险 目前,DNS面临的安全问题主要可以分为三类:DNS欺骗攻击.拒绝服务攻击.系统漏洞,下文将分别进行介绍.  DNS欺骗攻击 当一个DNS服务器遭到欺骗攻击,使用了来自一个恶 ...