前段时间,我在的项目组准备做一个类似美团外卖的拼手气红包【第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 }
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 }
===============后续
致谢:感谢您的阅读!一些问题请跳转自 http://www.cnblogs.com/0201zcr/p/5942748.html