使用Redis实现分布式锁详解
日期: 2018-12-15 分类: 个人收藏 335次阅读
在讲分布式锁之呢,我们不妨先来说说什么是分布式系统。
在系统早期,用户量少,可能我们一个app的所有模块都存在与一个应用包,部署在一台机器上,这便是我们的单体应用架构。这种设计,如果用户访问量大,便很容易造成系统压力过大而导致的系统宕机,其次如果一个模块,比如支付功能bug或其他原因,便直接导致整个系统瘫痪。
为解决这个问题,可能会想到我们的集群部署。
集群部署配合配合负载均衡(负载算法),可以在很大程度上减少单台服务器访问压力,但是这种模式任然存在某一模块bug,导致整个应用挂死的现象,继而导致整个集群架构瘫痪。
继而分布式架构便应声而出,分布式架构有一个最大的好处便是可以防止我么的某一模块的瘫痪导致整个应用的挂死。
如图,就算我们的支付模块挂了,我们还是可以完成我们的登录,下单等其他操作,不会造成整个应用群宕机。
当然今天主要是讲我们的分布式锁的实现。对于如上的分布式架构,会有一个什么问题呢?见下图
如图,如果我们系统是分布式架构,举个简单例子现在又库存模块和订单模块,如果库存只有1,但是此时同时有两个用户下单成功,都同时进入库存去做减库存操作,便会出现并发问题。我们可以用简单的代码来模拟这个操作。
如图,新建order类,模拟我们的订单系统,stock类,模拟我们的库存操作,库存只有一个,显然只有STOCK_NUM>0的时候才可以减库存成功。
新增测试类,模拟用户操作,期间起两个线程,模拟两个用户同时操作,下单后进行减库存,显然出现了并发问题,两个用户都减库存成功。那么这个问题要怎么解决呢,于是便引入我们今天的主角了,分布式锁了。
public class RedisLock implements Lock {
ThreadLocal<Jedis> jedis = new ThreadLocal<Jedis>();
private static String LOCK_NAME = "LOCK";
private static String REQUEST_ID = "111111";
void init(){
if (jedis.get() ==null)
jedis.set(new Jedis("localhost"));
}
public void lock() {
init();
if (tryLock())
jedis.get().set(LOCK_NAME,REQUEST_ID,"NX","PX",5000);
}
public boolean tryLock() {
while(true){
Long ret = jedis.get().setnx(LOCK_NAME,REQUEST_ID);
if (ret==1)
return true;
}
}
public void unlock() {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
jedis.get().eval(script, Collections.singletonList(LOCK_NAME), Collections.singletonList(REQUEST_ID));
}
public Condition newCondition() {
return null;
}
public void lockInterruptibly() throws InterruptedException {
}
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
}
代码如上,既然是锁,当然首先需要实现我们的Lock接口,当然对于里面的方法,我们只需要实现3个方法即可:tryLock(),lock()和unlock()即可。
public void lock() {
init();
if (tryLock())
jedis.get().set(LOCK_NAME,REQUEST_ID,"NX","PX",5000);
}
对于lock方法,很简单,都是固定套路的,先尝试拿锁,如果能拿到锁,则往下执行,否则阻塞,在设置key的时候,设置一个超时时间,防止系统死锁问题。
jedis.set(LOCK_NAME, REQUEST_ID);
jedis.expire(LOCK_NAME, 3000);
有人说是不是可以用上述写法,其实这种写法是有问题的,就不是原子操作了,如果在
jedis.set(LOCK_NAME, REQUEST_ID);执行完之后,系统挂死了,超时时间设置不了,后续解锁操作当然也没法执行了,则系统便一直死锁了。
考虑并发问题,对于jedis连接,采用ThreadLocal去构造。
ThreadLocal<Jedis> jedis = new ThreadLocal<Jedis>();
void init(){
if (jedis.get() ==null)
jedis.set(new Jedis("localhost"));
}
对于tryLock(),我们可以使用一个while(true)循环,去一直尝试拿锁,如果拿到,则返回结果标识
public boolean tryLock() {
while(true){
Long ret = jedis.get().setnx(LOCK_NAME,REQUEST_ID);
if (ret==1)
return true;
}
}
可能有人会问,为什么这里不用 jedis.get().get(LOCK_NAME)去判断,这里可以想一下,如果我们两个线程同时执行到tryLock(),但是没有执行lock(),redis里面没有对应的key,这时会出现两个tryLock().get(LOCK_NAME)的值都是false,然后导致两个线程都可以拿到锁的现象,导致线程安全问题,但是用setnx就不会。会先去判断key是否存在然后去设置,所有这时候肯定不会出现相关问题。
对于unlock()操作
public void unlock() {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
jedis.get().eval(script, Collections.singletonList(LOCK_NAME), Collections.singletonList(REQUEST_ID));
}
当然对于unlock()操作,也是一样的,为了保证原子性操作,采用脚本方式去执行操作,保证原子性。当然这里的原子性还是为了防止宕机操作,如果执行到unlock(),del操作之前系统挂死了,则这个key就不会被删除了,便会导致系统死锁问题。
String value = jedis.get(LOCK_NAME);
if (REQUEST_ID.equals(value)) {
jedis.del(LOCK_NAME);
}
上述这种写法也是有问题的,以后写redis分布式锁的时候还需注意。
至此,我们一个redis的分布式锁就写好了,一直执行也不会有线程安全问题了。
除特别声明,本站所有文章均为原创,如需转载请以超级链接形式注明出处:SmartCat's Blog
精华推荐