• Redis分布式锁的实现


      前段时间,我在的项目组准备做一个类似美团外卖的拼手气红包【第X个领取的人红包最大】,基本功能实现后,就要考虑这一操作在短时间内多个用户争抢同一资源的并发问题了,类似于很多应用如淘宝、京东的秒杀活动场景。所谓的秒杀就是多个线程对资源进行操作,而实现秒杀,就必须控制线程对资源的争抢。

    传统方法

      而最传统简单暴力的方法就是在秒杀的业务关键代码块外用Java的synchronized关键字锁住,但这种方式下的锁粒度比较高,比如两个线程同时执行秒杀方法,这两个线程操作的是不同的商品,从业务上讲应该是可以同时进行的,而两个线程会去争抢同一个锁,这是没必要的,而且synchronized是线程同步锁,只允许一个进程的一个线程访问,分布式场景下无法控制同步。这时候,分布式锁上场了。

    场景

      论分布式锁,查阅了很多资料,有很多方法可以实现,如zookeeper、redis等等,而他们的共同点都是通过状态值来标识锁,进而通过状态值来实现锁的占用与释放。比如现在有一个秒杀场景,db有一张表,对应有商品ID和库存,秒杀成功库存-1,现有500个线程秒杀商品1,另有500个线程秒杀商品2。通常具有秒杀场景的业务系统都比较复杂,承载的业务量非常巨大,并发量也很高。这样的系统往往采用分布式的架构来均衡负载。那么这1000个并发就会是从不同的地方过来,商品库存就是共享的资源,也是这1000个并发争抢的资源,这个时候我们需要将并发互斥管理起来,把和商品ID相关的字符串作为状态值来标识锁,这样就只有争抢同一商品的线程互斥,不会导致所以线程互斥。

      下面介绍下redis分布式锁的实现。

    实现原理   

      Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系。redis的setnx命令可以方便的实现分布式锁。

    三个命令  

       setNx key value  [key不存在时设置对应value;key已存在不做任何操作;意‘set if not exists’]

       get  [key不存在返回nil;key已存在返回值]

            getSet  [设置key值为value,并返回key的旧值]

    上代码就是一把梭[锁]

     1 package com.pagoda.eshop.customer.redis.lock;
     2 
     3 /**
     4  * 分布式锁接口
     5  * @Author: 小海
     6  * @Description:
     7  * @Date: Create in 17:28 2017/11/8
     8  */
     9 public interface IRedisLock {
    10 
    11     /**
    12      * 获取锁
    13      * @param lockKey
    14      * @return
    15      * @throws InterruptedException
    16      */
    17     boolean lock(String lockKey) throws InterruptedException;
    18 
    19     /**
    20      * 释放锁
    21      * @param lockKey
    22      */
    23     void unlock(String lockKey);
    24 }
    View Code
      1 package com.pagoda.eshop.customer.redis.lock.impl;
      2 
      3 import com.pagoda.eshop.customer.redis.lock.IRedisLock;
      4 import org.slf4j.Logger;
      5 import org.slf4j.LoggerFactory;
      6 import org.springframework.beans.factory.annotation.Autowired;
      7 import org.springframework.stereotype.Service;
      8 import redis.clients.jedis.JedisCluster;
      9 
     10 /**
     11  * 基于redis实现分布式锁
     12  * 备注-> https://www.cnblogs.com/novaCN/p/6417330.html[类模板/方法模板的配置-电商代码规范]
     13  * @Author: 小海
     14  * @Description:
     15  * @Date: Create in 17:28 2017/11/8
     16  */
     17 @Service
     18 public class RedisLock implements IRedisLock{
     19 
     20     private Logger logger = LoggerFactory.getLogger(this.getClass());
     21 
     22     @Autowired
     23     private JedisCluster jedis;
     24 
     25     private String lockKey;
     26 
     27     private static final int DEFAULT_LOOP_INTERVAL_MILLIS = 100;
     28 
     29     /**
     30      * 锁超时时间,防止线程在入锁以后,无限的执行等待
     31      */
     32     private int expireMsecs = 5 * 1000;
     33 
     34     /**
     35      * 锁等待时间,防止线程饥饿死锁
     36      */
     37     private int timeoutMsecs = 10 * 1000;
     38 
     39     /**
     40      * 锁标识
     41      */
     42     private volatile boolean locked = false;
     43 
     44     /**
     45      * 锁key后缀
     46      */
     47     private static final String LOCKKEY_SUFFIX = ":lock";
     48 
     49     private String get(final String key) {
     50         Object obj = null;
     51         try {
     52             obj = jedis.get(key);
     53         } catch (Exception e) {
     54             logger.error("get redis error, key : ", key);
     55         }
     56         return obj == null ? null : obj.toString();
     57     }
     58 
     59     /**
     60      * 若key不存在,将key的值设为value,并返回true
     61      * 若key已经存在,则setnx不做任何动作,并返回false
     62      * @param key
     63      * @param value
     64      * @return
     65      */
     66     private boolean setNX(final String key, final String value) {
     67         Object obj = null;
     68         try {
     69             obj = jedis.setnx(key, value);
     70         } catch (Exception e) {
     71             logger.error("setNX redis error, key : ", key);
     72         }
     73         return ((Long) obj).intValue() == 0 ? false : true;
     74     }
     75 
     76     /**
     77      * 设置现在的锁到期时间并返回上一个锁到期时间
     78      * @param key
     79      * @param value
     80      * @return 上一个锁的到期时间
     81      */
     82     private String getSet(final String key, final String value) {
     83         Object obj = null;
     84         try {
     85             obj = jedis.getSet(key, value);
     86         } catch (Exception e) {
     87             logger.error("getSet redis error, key : ", key);
     88         }
     89         return obj == null ? null : (String) obj;
     90     }
     91 
     92     /**
     93      * 获取锁
     94      *
     95      * 实现思路:
     96      * 主要是使用了redis的setnx命令,缓存了锁
     97      * reids缓存的key是锁的key,所有的共享,value是锁的到期时间
     98      *
     99      * 执行过程:
    100      * 1.通过setnx尝试设置某个key的值,若锁不存在,则返回true,成功获得锁
    101      * 2.若锁已经存在,则通过get获取锁的到期时间,和当前时间比较,超时的话,则通过getset设置新的值并返回上一个线程锁的到期时间
    102      * 3.若通过get和getset获取到的线程锁的到期时间一致的话,则返回true,成功获得锁
    103      * 4.若无法满足1或3的条件,则睡眠一小段时间,一定时间内循环1~3操作,尝试加锁
    104      * 5.若超出锁等待时间,则返回false,获取锁失败
    105      *
    106      * @return 若获得锁,返回true;若执行超时,返回false
    107      * @throws InterruptedException
    108      */
    109     @Override
    110     public boolean lock(String lockKey) throws InterruptedException {
    111         lockKey = lockKey + LOCKKEY_SUFFIX;
    112         int timeout = timeoutMsecs;
    113         while (timeout >= 0) {
    114             long expires = System.currentTimeMillis() + expireMsecs + 1;
    115             // 对加锁做时效性检测,设置锁到期时间
    116             String expiresStr = String.valueOf(expires);
    117             if (this.setNX(lockKey, expiresStr)) {
    118                 // 成功获得锁
    119                 locked = true;
    120                 return true;
    121             }
    122             // 当前锁的到期时间
    123             String currentValueStr = this.get(lockKey);
    124             // 若锁已超时[获取锁的客户端执行时间过长,进程被kill掉,或因为其他异常崩溃,导致无法释放锁,就会造成死锁],则重新加锁
    125             if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
    126                 // 如果多个线程同时走到这里,但是走到这里时每个线程拿到的oldValueStr肯定不可能一样
    127                 String oldValueStr = this.getSet(lockKey, expiresStr);
    128 
    129                 // 如果多个线程同时走到这里,但是只有一个线程的设置值和当前值相同,他才有权利获取锁
    130                 if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
    131                     // 成功获得锁
    132                     locked = true;
    133                     return true;
    134                 }
    135             }
    136             timeout -= DEFAULT_LOOP_INTERVAL_MILLIS;
    137             Thread.sleep(DEFAULT_LOOP_INTERVAL_MILLIS);
    138         }
    139         return false;
    140     }
    141 
    142     /**
    143      * 释放锁
    144      */
    145     @Override
    146     public void unlock(String lockKey) {
    147         if (locked) {
    148             jedis.del(lockKey);
    149             locked = false;
    150         }
    151     }
    152 }
    View Code

    ===============后续

    致谢:感谢您的阅读!一些问题请跳转自 http://www.cnblogs.com/0201zcr/p/5942748.html

  • 相关阅读:
    Linux-线程同步(day14续)
    Linux之线程(day14)
    Linux-网络编程-UDP网络编程(day13续2)
    ES6 模块加载
    let与var声明区别
    vue 常用指令v-if v-else v-show v-for
    动态路由的意义,以及路由重定向
    前端路由的理解
    socpe 与 包的引入
    VUE 组件注册(全局、局部)
  • 原文地址:https://www.cnblogs.com/xhyouyou/p/8299333.html
Copyright © 2020-2023  润新知