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

posted @ 2018-01-16 23:09  要好好吃饭  阅读(474)  评论(0编辑  收藏  举报