当我们在单机情况下,遇到并发问题,可以使用juc包下的lock锁,或者synchronized关键字来加锁。但是这俩都是JVM级别的锁,如果跨了JVM这两个锁就不能控制并发问题了,也就是说在分布式集群环境中,需要寻求其他方法来解决并发问题。前面也说到可以使用redis的setnx操作,如果不存在则set,如果存在则不set。也就是说每个服务实例都对同一个key进行操作。谁能set成功就认为获取到了锁。可以执行下面的操作。执行完之后释放锁。如下按照上述逻辑来简单实现一个分布式锁:

package com.nijunyang.redis.lock;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController; /**
* Description:
* Created by nijunyang on 2020/3/17 23:53
*/
@RestController
public class LockController { @Autowired
ValueOperations<String, Object> valueOperations; @Autowired
RedisTemplate<String, Object> redisTemplate; String lock = "lock"; String quantityKey = "quantity"; @GetMapping("/deduct-stock")
public String deductStock() {
try {
boolean getLock = valueOperations.setIfAbsent(lock, 1);
if (!getLock) {
return "没有获取到锁";
}
//使用当做数据库,只是模拟扣减库存场景,因此不使用原子操作
Integer quantity = (Integer) valueOperations.get(quantityKey);
if (quantity > 0) {
--quantity;
valueOperations.set(quantityKey, quantity);
System.out.println("扣减库存成功,剩余库存: " + quantity);
} else {
System.out.println("扣减库存成功,剩余库存: " + quantity);
}
return "true";
} finally {
redisTemplate.delete(lock);
}
} }

如果不出意外这个锁是可以用的,但是如果拿到锁之后,在执行业务的过程中,服务挂了,就会导致锁没有释放,其他服务永远无法拿到锁,因此我们可以优化一下,加锁的同时给锁设置一个过期时间,这样来保证,拿到锁在执行业务的时候挂了,到了过期时间之后,其他服务一样可以继续获取锁。

package com.nijunyang.redis.lock;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController; import java.util.concurrent.TimeUnit; /**
* Description:
* Created by nijunyang on 2020/3/17 23:53
*/
@RestController
public class LockController { @Autowired
ValueOperations<String, Object> valueOperations; @Autowired
RedisTemplate<String, Object> redisTemplate; String lock = "lock"; String quantityKey = "quantity"; @GetMapping("/deduct-stock")
public String deductStock() {
try {
//设置值,并且设置超时时间
boolean getLock = valueOperations.setIfAbsent(lock, 1, 10, TimeUnit.SECONDS);
if (!getLock) {
return "没有获取到锁";
}
//使用当做数据库,只是模拟扣减库存场景,因此不使用原子操作
Integer quantity = (Integer) valueOperations.get(quantityKey);
if (quantity > 0) {
--quantity;
valueOperations.set(quantityKey, quantity);
System.out.println("扣减库存成功,剩余库存: " + quantity);
} else {
System.out.println("扣减库存成功,剩余库存: " + quantity);
}
return "true";
} finally {
redisTemplate.delete(lock);
}
} }

但是问题又来了,这个超时时间设置多大合适呢,如果网络延迟或者出现了sql的慢查询等,导致业务还没执行完,锁就过期了,这个时候别的服务又拿到了锁,现在并发问题问题又来了。。。A1服务拿到锁,设置过期时间10s,但是业务逻辑需要15s才能执行完,10s过后锁自动释放,这时候A2服务拿到锁执行业务,5s之后A1执行完业务删除锁,但是这个时候A1释放的是A2加的锁,A2这个时候才执行5s,等到A2执行完去释放的又是别的服务拿到的锁,如此恶心循环。。。。

我们可以将锁的value设置成一个客户端的唯一值,比如生成一个UUID,删除的时候判断一下这个值是否是自己生成,这样就可以避免把其他服务加的锁删掉。

package com.nijunyang.redis.lock;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController; import java.util.UUID;
import java.util.concurrent.TimeUnit; /**
* Description:
* Created by nijunyang on 2020/3/17 23:53
*/
@RestController
public class LockController { @Autowired
ValueOperations<String, Object> valueOperations; @Autowired
RedisTemplate<String, Object> redisTemplate; String lock = "lock"; String quantityKey = "quantity"; @GetMapping("/deduct-stock")
public String deductStock() {
String uuid = UUID.randomUUID().toString();
try {
//设置值,并且设置超时时间
boolean getLock = valueOperations.setIfAbsent(lock, uuid, 10, TimeUnit.SECONDS);
// boolean getLock = valueOperations.setIfAbsent(lock, 1);
if (!getLock) {
return "没有获取到锁";
}
//使用当做数据库,只是模拟扣减库存场景,因此不使用原子操作
Integer quantity = (Integer) valueOperations.get(quantityKey);
if (quantity > 0) {
--quantity;
valueOperations.set(quantityKey, quantity);
System.out.println("扣减库存成功,剩余库存: " + quantity);
} else {
System.out.println("扣减库存成功,剩余库存: " + quantity);
}
return "true";
} finally {
//删除之前判断是否是自己加的锁
if (uuid.equals(valueOperations.get(lock))) {
redisTemplate.delete(lock);
}
}
} }

这样只是保证自己的锁不被别人删掉,但是这个判断再删除的操作也不是原子操作,同时超时的问题还是没有解决。怎么办呢,我们给锁续命,可以在加锁的同时再起一个定时任务,去检查锁是否释放,如果没有释放就增加超时时间,然后再去定时检查,直到锁被删除了。比如锁超时时间10s,那么定时任务在8s后去检查,锁是否被释放,如果没有释放则重新设置超时时间。继续监视锁是否释放。

如果我们自己按照这个逻辑去实现,有可能还会有很多bug。Redisson已经帮我们很好的实现了分布式锁。配置好之后,使用就像使用java的lock一样。原理就和上述差不多。

加依赖:

<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.6.5</version>
</dependency>

写配置:

@Bean
public Redisson redisson() {
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379").setPassword("xxx"); /**
* 哨兵
*/
//config.useSentinelServers().addSentinelAddress(""); /**
* 集群
*/
//config.useClusterServers().addNodeAddress("redis://111.229.53.45:6379");
return (Redisson) Redisson.create(config);
}

使用:

package com.nijunyang.redis.lock;

import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController; import java.util.UUID;
import java.util.concurrent.TimeUnit; /**
* Description:
* Created by nijunyang on 2020/3/17 23:53
*/
@RestController
public class LockController { @Autowired
ValueOperations<String, Object> valueOperations; @Autowired
RedisTemplate<String, Object> redisTemplate; @Autowired
Redisson redisson; String lockKey = "lockKey"; String quantityKey = "quantity"; @GetMapping("/deduct-stock2")
public String deductStock2() {
RLock redissonLock = redisson.getLock(lockKey);
try {
redissonLock.lock();
//使用当做数据库,只是模拟扣减库存场景,因此不使用原子操作
Integer quantity = (Integer) valueOperations.get(quantityKey);
if (quantity > 0) {
--quantity;
valueOperations.set(quantityKey, quantity);
System.out.println("扣减库存成功,剩余库存: " + quantity);
return "true";
} else {
System.out.println("扣减库存成功,剩余库存: " + quantity);
return "false";
}
} finally {
redissonLock.unlock();
}
} }

和JUC包里面Lock锁的使用一模一样,有木有?

Redisson锁源码逻辑简要分析,直接在代码中加的注释说明,里面大量使用lua脚本来封装redis操作的原子性,上面提到的判断再删除的操作,也可以写成lua脚本执行,保证原子性。同时lua脚本中如果出错了,数据还会回滚。

虽然看起来已经很完善了,但是还有一点点问题如果哨兵模式,或者集群模式,锁加载master上面,还未同步到slave的时候,master挂了,这个重新选举,新的master上面是没有加锁的。不过这种几率已经很小很小了,如果是在要求强一致性,那么就只有选择zookeeper来实现,因为zookeeper是强一致性的,它是多数节点数据都同步好了才返回。Master挂了,选举也是在数据一致的节点中,因此重新选上来leader肯定是有锁的。当然ZK的性能肯定就没有redis的高了,怎么选择还是看自己业务是否允许。

Redisson也提供了一个RedissonRedLock,传入多个锁对象,加锁的时候,多个锁都加上才认为加锁成功。但是这样需要连接多个redis。这样肯定是有性能问题的,还有网络问题等等。

浅析Redis分布式锁---从自己实现到Redisson的实现的更多相关文章

  1. 七种方案!探讨Redis分布式锁的正确使用姿势

    前言 日常开发中,秒杀下单.抢红包等等业务场景,都需要用到分布式锁.而Redis非常适合作为分布式锁使用.本文将分七个方案展开,跟大家探讨Redis分布式锁的正确使用方式.如果有不正确的地方,欢迎大家 ...

  2. 死磕 java同步系列之redis分布式锁进化史

    问题 (1)redis如何实现分布式锁? (2)redis分布式锁有哪些优点? (3)redis分布式锁有哪些缺点? (4)redis实现分布式锁有没有现成的轮子可以使用? 简介 Redis(全称:R ...

  3. 《Redis 分布式锁》

    一:什么是分布式锁. -  通俗来说的话,就是在分布式架构的redis中,使用锁. 二:分布式锁的使用选择. - 当 Redis 的使用场景不多,而且也只是单个在用的时候,可以构建自己使用的 锁. - ...

  4. Redisson实现Redis分布式锁的底层原理

    一.写在前面 现在面试,一般都会聊聊分布式系统这块的东西.通常面试官都会从服务框架(Spring Cloud.Dubbo)聊起,一路聊到分布式事务.分布式锁.ZooKeeper等知识.所以咱们这篇文章 ...

  5. 面试必问:如何实现Redis分布式锁

    摘要:今天我们来聊聊分布式锁这块知识,具体的来看看Redis分布式锁的实现原理. 一.写在前面 现在面试,一般都会聊聊分布式系统这块的东西.通常面试官都会从服务框架(Spring Cloud.Dubb ...

  6. 利用redis分布式锁的功能来实现定时器的分布式

    文章来源于我的 iteye blog http://ak478288.iteye.com/blog/1898190 以前为部门内部开发过一个定时器程序,这个定时器很简单,就是配置quartz,来实现定 ...

  7. Redis分布式锁

    Redis分布式锁 分布式锁是许多环境中非常有用的原语,其中不同的进程必须以相互排斥的方式与共享资源一起运行. 有许多图书馆和博客文章描述了如何使用Redis实现DLM(分布式锁管理器),但是每个库都 ...

  8. redis分布式锁和消息队列

    最近博主在看redis的时候发现了两种redis使用方式,与之前redis作为缓存不同,利用的是redis可设置key的有效时间和redis的BRPOP命令. 分布式锁 由于目前一些编程语言,如PHP ...

  9. redis咋么实现分布式锁,redis分布式锁的实现方式,redis做分布式锁 积极正义的少年

    前言 分布式锁一般有三种实现方式:1. 数据库乐观锁:2. 基于Redis的分布式锁:3. 基于ZooKeeper的分布式锁.本篇博客将介绍第二种方式,基于Redis实现分布式锁.虽然网上已经有各种介 ...

随机推荐

  1. Java IO: 并发IO

    原文链接 作者: Jakob Jenkov 译者: 李璟 有时候你可能需要并发地处理输入和输出.换句话说,你可能有超过一个线程处理输入和产生输出.比如,你有一个程序需要处理磁盘上的大量文件,这个任务可 ...

  2. c++ 如何清除上一次的输出?

    #include <iostream.h>#include <stdlib.h>int main(){cout<<"PBY PBY PBY PBY PBY ...

  3. 《自动化平台测试开发-Python测试开发实战》新书出版了

    首先 第一本书,当初在百度阅读初步写了个电子版,刚一上线不久即收到了数百位读者朋友阅读收藏购买,于是顺利成章就出版了纸质书. <软件自动化测试开发>认真看过的读者应该都知道,介绍的主要是自 ...

  4. xpath_note - Ethan Lee

    https://ethan2lee.github.io/ XPath概览 XPath,全称XML Path Language,即XML路径语言,它是一门在XML文档中查找信息的语言.它最初是用来搜寻X ...

  5. 两步解决maven plugins 插件下载慢 !下载报红的问题!

    两步解决maven plugins 插件下载慢 !下载报红的问题! 1.找到你解压的maven安装路径下的conf   编辑settings 2.添加如下   使用阿里的 <mirror> ...

  6. 漫说测试 | 研发虐我千百遍,我待bug如初恋

    的行业之一他们的运筹帷幄,他们的勾心斗角,只有自己知道.000,但绝对也是最枯燥的行业之一! IT可能是几个最高薪行业之一,但同时也绝对是最辛苦的行业之一!IT业是最需要创新能力的行业之一,但绝对也是 ...

  7. 树的三种DFS策略(前序、中序、后序)遍历

    之前刷leetcode的时候,知道求排列组合都需要深度优先搜索(DFS), 那么前序.中序.后序遍历是什么鬼,一直傻傻的分不清楚.直到后来才知道,原来它们只是DFS的三种不同策略. N = Node( ...

  8. 关于配置cordova的一些细节

    网上多数资料都是:安装nodejs->通过node js安装cordova->JDK->设置环境变量JAVA_HOME->安装android SDK->设置环境变量AND ...

  9. TCP传输连接管理

    TCP传输连接管理 一.传输连接的三个阶段 1.1.概述 传输连接就有三个阶段,即:连接建立.数据传送和连接释放. 连接建立过程中要解决以下三个问题: 要使每一方能够确知对方的存在. 要允许双方协商一 ...

  10. linux入门系列16--文件共享之Samba和NFS

    前一篇文章"linux入门系列15--文件传输之vsftp服务"讲解了文件传输,本篇继续讲解文件共享相关知识. 文件共享在生活和工作中非常常见,比如同一团队中不同成员需要共同维护同 ...