redis分布式锁的应用场景_redis为什么可以做分布式锁

redis分布式锁的应用场景_redis为什么可以做分布式锁“分布式锁”是用来解决分布式应用中“并发冲突”的一种常用手段,实现方式一般有基于zookeeper及基于redis二种。具体到业务场景中,我们要考虑二种情况:一、抢不到锁的请求,允许丢弃(即:忽略)比如:一些不是很重要的场景,比如“监控数据持续上报”,某一篇文章的“已读/未读”标识位更新,对于同一个id,如果并发的请求同时到达,只要有一个请求处理成功,就算成功。用活动图表示如下:二、并发请求,不论哪一条都必须要处理的场景(即:不允许丢数据)比如:一个订单,客户正在前台修改地址,.

大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。

Jetbrains全系列IDE使用 1年只要46元 售后保障 童叟无欺

“分布式锁”是用来解决分布式应用中“并发冲突”的一种常用手段,实现方式一般有基于zookeeper及基于redis二种。

这里我们分析下基于redis得场景和实现。

单节点部署场景

  • 举例说明,系统A和系统B是两个部署在不同节点的相同应用(集群部署),这时客户端请求传来,两个系统都受到了请求,并且该请求是对数据表进行插入操作,如果这个时候不加锁来控制,可能会导致数据库新增两条记录,这时系统也不能允许的,由于是在不同应用内,在单个应用内加JVM级别的锁,另一个应用是感知不到的,这时需要用到分布式锁。
  • 接下来我们看看这种场景如何实现安全的分布式锁,由于是单节点部署场景,我们可以用setnx命令,以请求的唯一主键作为key,由于该操作是原子操作,当系统A设值成功后,系统B是无法设置成功的, 这时A就可以进行查询并插入操作,操作数据库完成后,删除key,此时系统B才能设值成功,但是由于查询到数据库有记录,所以并不会插入数据,这样就解决了该问题。但是这里会有个问题,如果redis挂机了,这里的锁不是永远都不释放了吗, 所以为了解决这个问题,redis提供了set命令,可传入超时时间的,那么在指定的时间范围内,如果没有释放锁,则该锁自动过期。如果执行时间超过超时时间呢,比如系统A还未执行完任务,就释放了锁,系统B接着执行任务,这时,系统A执行完了,把锁删掉(此时删除的时系统B获取的锁)。
    • 方案一: 为了避免这种情况,在del锁之前可以做一个判断,验证key对应的value是不是自己线程的ID.如果要考虑原子性问题,可以使用Lua脚本来实现,保证验证和删除的原子性。
    • 方案二:我们可以让获得锁的线程开启一个守护线程,用来给快要过期的锁加长超时时间。当系统A中的线程执行完任务,再显式关掉守护线程。

具体到业务场景中,我们要考虑二种情况:

一、抢不到锁的请求,允许丢弃(即:忽略)

比如:一些不是很重要的场景,比如“监控数据持续上报”,某一篇文章的“已读/未读”标识位更新,对于同一个id,如果并发的请求同时到达,只要有一个请求处理成功,就算成功。

用活动图表示如下:

点击查看原图

 

二、并发请求,不论哪一条都必须要处理的场景(即:不允许丢数据)

比如:一个订单,客户正在前台修改地址,管理员在后台同时修改备注。地址和备注字段的修改,都必须正确更新,这二个请求同时到达的话,如果不借助db的事务,很容易造成行锁竞争,但用事务的话,db的性能显然比不上redis轻量。

解决思路:A,B二个请求,谁先抢到分布式锁(假设A先抢到锁),谁先处理,抢不到的那个(即:B),在一旁不停等待重试,重试期间一旦发现获取锁成功,即表示A已经处理完,把锁释放了。这时B就可以继续处理了。

但有二点要注意:

a、需要设置等待重试的最长时间,否则如果A处理过程中有bug,一直卡死,或者未能正确释放锁,B就一直会等待重试,但是又永远拿不到锁。

b、等待最长时间,必须小于锁的过期时间。否则,假设锁2秒过期自动释放,但是A还没处理完(即:A的处理时间大于2秒),这时锁会因为redis key过期“提前”误释放,B重试时拿到锁,造成A,B同时处理。(注:可能有同学会说,不设置锁的过期时间,不就完了么?理论上讲,确实可以这么做,但是如果业务代码有bug,导致处理完后没有unlock,或者根本忘记了unlock,分布式锁就会一直无法释放。所以综合考虑,给分布式锁加一个“保底”的过期时间,让其始终有机会自动释放,更为靠谱)

用活动图表示如下:

点击查看原图

写了一个简单的工具类:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

package com.cnblogs.yjmyzz.redisdistributionlock;

 

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.data.redis.core.StringRedisTemplate;

import org.springframework.util.StringUtils;

 

import java.util.UUID;

import java.util.concurrent.TimeUnit;

 

/**

 * 利用redis获取分布式锁

 *

 * @author 菩提树下的杨过

 * @blog http://yjmyzz.cnblogs.com/

 */

public class RedisLock {

 

    private StringRedisTemplate redisTemplate;

 

    private Logger logger = LoggerFactory.getLogger(this.getClass());

 

    /**

     * simple lock尝试获取锅的次数

     */

    private int retryCount = 3;

 

    /**

     * 每次尝试获取锁的重试间隔毫秒数

     */

    private int waitIntervalInMS = 100;

 

 

    public RedisLock(StringRedisTemplate redisTemplate) {

        this.redisTemplate = redisTemplate;

    }

 

    /**

     * 利用redis获取分布式锁(未获取锁的请求,允许丢弃!)

     *

     * @param redisKey       锁的key值

     * @param expireInSecond 锁的自动释放时间(秒)

     * @return

     * @throws DistributionLockException

     */

    public String simpleLock(final String redisKey, final int expireInSecond) throws DistributionLockException {

        String lockValue = UUID.randomUUID().toString();

        boolean flag = false;

        if (StringUtils.isEmpty(redisKey)) {

            throw new DistributionLockException("key is empty!");

        }

        if (expireInSecond <= 0) {

            throw new DistributionLockException("expireInSecond must be bigger than 0");

        }

        try {

            for (int i = 0; i < retryCount; i++) {

                boolean success = redisTemplate.opsForValue().setIfAbsent(redisKey, lockValue, expireInSecond, TimeUnit.SECONDS);

                if (success) {

                    flag = true;

                    break;

                }

                try {

                    TimeUnit.MILLISECONDS.sleep(waitIntervalInMS);

                catch (Exception ignore) {

                    logger.warn("redis lock fail: " + ignore.getMessage());

 

                }

            }

            if (!flag) {

                throw new DistributionLockException(Thread.currentThread().getName() + " cannot acquire lock now ...");

            }

            return lockValue;

        catch (DistributionLockException be) {

            throw be;

        catch (Exception e) {

            logger.warn("get redis lock error, exception: " + e.getMessage());

            throw e;

        }

    }

 

    /**

     * 利用redis获取分布式锁(未获取锁的请求,将在timeoutSecond时间范围内,一直等待重试)

     *

     * @param redisKey       锁的key值

     * @param expireInSecond 锁的自动释放时间(秒)

     * @param timeoutSecond  未获取到锁的请求,尝试重试的最久等待时间(秒)

     * @return

     * @throws DistributionLockException

     */

    public String lock(final String redisKey, final int expireInSecond, final int timeoutSecond) throws DistributionLockException {

        String lockValue = UUID.randomUUID().toString();

        boolean flag = false;

        if (StringUtils.isEmpty(redisKey)) {

            throw new DistributionLockException("key is empty!");

        }

        if (expireInSecond <= 0) {

            throw new DistributionLockException("expireInSecond must be greater than 0");

        }

        if (timeoutSecond <= 0) {

            throw new DistributionLockException("timeoutSecond must be greater than 0");

        }

        if (timeoutSecond >= expireInSecond) {

            throw new DistributionLockException("timeoutSecond must be less than expireInSecond");

        }

        try {

            long timeoutAt = System.currentTimeMillis() + timeoutSecond * 1000;

            while (true) {

                boolean success = redisTemplate.opsForValue().setIfAbsent(redisKey, lockValue, expireInSecond, TimeUnit.SECONDS);

                if (success) {

                    flag = true;

                    break;

                }

                if (System.currentTimeMillis() >= timeoutAt) {

                    break;

                }

                try {

                    TimeUnit.MILLISECONDS.sleep(waitIntervalInMS);

                catch (Exception ignore) {

                    logger.warn("redis lock fail: " + ignore.getMessage());

                }

            }

            if (!flag) {

                throw new DistributionLockException(Thread.currentThread().getName() + " cannot acquire lock now ...");

            }

            return lockValue;

        catch (DistributionLockException be) {

            throw be;

        catch (Exception e) {

            logger.warn("get redis lock error, exception: " + e.getMessage());

            throw e;

        }

    }

 

 

    /**

     * 锁释放

     *

     * @param redisKey

     * @param lockValue

     */

    public void unlock(final String redisKey, final String lockValue) {

        if (StringUtils.isEmpty(redisKey)) {

            return;

        }

        if (StringUtils.isEmpty(lockValue)) {

            return;

        }

        try {

            String currLockVal = redisTemplate.opsForValue().get(redisKey);

            if (currLockVal != null && currLockVal.equals(lockValue)) {

                boolean result = redisTemplate.delete(redisKey);

                if (!result) {

                    logger.warn(Thread.currentThread().getName() + " unlock redis lock fail");

                else {

                    logger.info(Thread.currentThread().getName() + " unlock redis lock:" + redisKey + " successfully!");

                }

            }

        catch (Exception je) {

            logger.warn(Thread.currentThread().getName() + " unlock redis lock error:" + je.getMessage());

        }

    }

}

  

然后写个spring-boot来测试一下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

package com.cnblogs.yjmyzz.redisdistributionlock;

 

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.context.ConfigurableApplicationContext;

import org.springframework.data.redis.core.StringRedisTemplate;

 

import java.util.concurrent.CountDownLatch;

import java.util.concurrent.TimeUnit;

 

@SpringBootApplication

public class RedisDistributionLockApplication {

 

    private static Logger logger = LoggerFactory.getLogger(RedisDistributionLockApplication.class);

 

    public static void main(String[] args) throws InterruptedException {

        ConfigurableApplicationContext applicationContext = SpringApplication.run(RedisDistributionLockApplication.class, args);

 

        //初始化

        StringRedisTemplate redisTemplate = applicationContext.getBean(StringRedisTemplate.class);

        RedisLock redisLock = new RedisLock(redisTemplate);

        String lockKey = "lock:test";

 

 

        CountDownLatch start = new CountDownLatch(1);

        CountDownLatch threadsLatch = new CountDownLatch(2);

 

        final int lockExpireSecond = 5;

        final int timeoutSecond = 3;

 

        Runnable lockRunnable = () -> {

            String lockValue = "";

            try {

                //等待发令枪响,防止线程抢跑

                start.await();

 

                //允许丢数据的简单锁示例

                lockValue = redisLock.simpleLock(lockKey, lockExpireSecond);

 

 

                //不允许丢数据的分布式锁示例

                //lockValue = redisLock.lock(lockKey, lockExpireSecond, timeoutSecond);

 

                //停一会儿,故意让后面的线程抢不到锁

                TimeUnit.SECONDS.sleep(2);

                logger.info(String.format("%s get lock successfully, value:%s", Thread.currentThread().getName(), lockValue));

 

            catch (Exception e) {

                e.printStackTrace();

            finally {

                redisLock.unlock(lockKey, lockValue);

                //执行完后,计数减1

                threadsLatch.countDown();

            }

 

        };

 

        Thread t1 = new Thread(lockRunnable, "T1");

        Thread t2 = new Thread(lockRunnable, "T2");

 

        t1.start();

        t2.start();

 

        //预备:开始!

        start.countDown();

 

        //等待所有线程跑完

        threadsLatch.await();

 

        logger.info("======>done!!!");

 

    }

 

}

 用2个线程模拟并发场景,跑起来后,输出如下:

点击查看原图

可以看到T2线程没抢到锁,直接抛出了预期的异常。

把44行的注释打开,即:换成不允许丢数据的模式,再跑一下:

点击查看原图

可以看到,T1先抢到锁,然后经过2秒的处理后,锁释放,这时T2重试拿到了锁,继续处理,最终释放。

 

文章参考:

基于redis的分布式锁二种应用场景

https://www.cnblogs.com/yjmyzz/

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/195984.html原文链接:https://javaforall.net

(0)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

  • ftp服务器映射到外网_内网穿透安全吗

    ftp服务器映射到外网_内网穿透安全吗转载:https://service.oray.com/question/752.html花生壳服务目前的用户量已达1600万以上,广泛应用于网站建设、视频监控、遥感测绘、FTP、VPN、企业OA、ERP等应用领域。花生壳服务可以将您的动态公网IP和域名进行实时绑定。简单来说,就像您的手机一样,无论在那里,只要通过一个号码就可以找到您了,而这个号码就相于在贝锐使用花生壳服务的域名。现在我们以实例讲解如何使用花生壳来协助搭建FTP服务,从而实现FTP服务的外网访问。需要使用的软件工具:花生壳8客户端和SE

    2025年10月30日
    2
  • Android sdk_安卓sdk工具下载

    Android sdk_安卓sdk工具下载1在官网上获得软件压缩包imx-android-10.0.0_2.5.0.tar.gz下载地址2获取源码拷贝imx-android-10.0.0_2.5.0.tar.gz到一个文件夹里并解压,进入:2.1获取repogitclonehttps://mirrors.tuna.tsinghua.edu.cn/git/git-repo修改repo里的内容为下面REPO_URL=’https://mirrors.bfsu.edu.cn/git/git-repo’然后添

    2022年8月30日
    3
  • youtube8m 第四届_youtube怎么上

    youtube8m 第四届_youtube怎么上今天进入前十

    2022年5月3日
    33
  • swagger2 注解说明 ( @ApiImplicitParams )

    swagger2 注解说明 ( @ApiImplicitParams )Api 用在请求的类上 表示对类的说明 tags 说明该类的作用 可以在 UI 界面上看到的注解 value 该参数没什么意义 在 UI 界面上也看到 所以不需要配置 ApiOperation 用在请求的方法上 说明方法的用途 作用 value 说明方法的用途 作用 notes 方法的备注说明 ApiImplicitP 用在请求的方

    2025年11月27日
    5
  • 微型计算机硬件系统的性能主要取决6,大学计算机基础单选试题「附答案」

    微型计算机硬件系统的性能主要取决6,大学计算机基础单选试题「附答案」大学计算机基础单选试题「附答案」一、单选题1.世界上首次提出存储程序计算机体系结构的是(D)A.莫奇莱B.艾仑·图灵C.乔治·布尔D.冯·诺依曼2计算机诞生于(B)A.1941年B.1946年C.1949年D.1950年3、世界上第一台电子数字计算机采用的主要逻辑部件是(A)A.电子管B.晶体管C.继电器D.光电管4、下列叙述正确的是(D)A.世界上第一台电子计算机ENIAC…

    2022年6月28日
    58
  • sqlbulkcopy 使用DataTable作为数据源的数据类型问题–来自数据源的String类型的给定值不能转换为指定目标列的类型 uniqueidentifier…

    sqlbulkcopy 使用DataTable作为数据源的数据类型问题–来自数据源的String类型的给定值不能转换为指定目标列的类型 uniqueidentifier…今天做批量插入的时候,SQLSERVER总是报错,错误提示“来自数据源的String类型的给定值不能转换为指定目标列的类型uniqueidentifier”。首先核对了一下定义的dataTable中的DataColumn[]的顺序和数量,发现和数据库的表结构是一致的,而且把代码中对dataRow[]对应位置赋值的语句屏蔽后,执行成功。因此可以确定主要还是由于类型转换的时候的问题。后来…

    2022年7月20日
    25

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号