一文读懂分布式锁-使用SpringBoot+Redis实现分布式锁解决方案

一文读懂分布式锁-使用SpringBoot+Redis实现分布式锁解决方案

随着现在分布式架构越来越盛行,在很多场景下需要使用到分布式锁。很多小伙伴对于分布式锁还不是特别了解,所以特地总结了一篇文章,让大家一文读懂分布式锁的前世今生。

分布式锁的实现有很多种,比如基于数据库、Redis 、 zookeeper 等实现,本文的示例主要介绍使用Redis实现分布式锁。

一、什么是分布式锁

分布式锁,即分布式系统中的锁,分布式锁是控制分布式系统有序地对共享资源进行操作,在单体应用中我们通过锁实现共享资源访问,而分布式锁,就是解决了分布式系统中控制共享资源访问的问题。

可能初学的小伙伴就会有疑问,Java多线程中的公平锁、非公平锁、自旋锁、可重入锁、读写锁、互斥锁这些都还没闹明白呢?怎么有出来一个分布式锁?

其实,可以这么理解:Java的原生锁是解决多线程下对于共享资源的操作,而分布式锁则是多进程下对于共享资源的操作。分布式系统中竞争共享资源的最小粒度从线程升级成了进程。

分布式锁已经被应用到各种高并发的场景下,典型场景案例包括:秒杀、车票、订单、退款、库存等场景。

二、为什么要使用分布式锁

在分布式系统中,常常需要协调他们的动作。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,这个时候,便需要使用到分布式锁。

目前几乎很多大型网站及应用都是分布式部署的,如何保证分布式场景中的数据一致性问题一直是一个比较重要的话题。在某些场景下,为了保证数据的完整性和一致性,我们需要保证一个方法在同一时间内只能被同一个线程执行,这就需要使用分布式锁。

如上图所示,假设用户A和用户B同时购买了某款商品,订单创建成功后,下单系统A和下单系统B就会同时对数据库中的该款商品的库存进行扣减。如果此时不加任何控制,系统B提交的数据更新就会覆盖系统A的数据,导致库存错误,超卖等问题。

三、分布式锁应该具备哪些条件

在介绍分布式锁的实现方式之前,先了解一下分布式锁应该具备哪些条件:

1、在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行;

2、高可用的获取锁与释放锁;

3、高性能的获取锁与释放锁;

4、具备可重入特性;

5、具备锁失效机制,防止死锁;

6、具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。

四、分布式锁的实现方式

随着业务发展的需要,原来的单体应用被演化成分布式集群系统后,由于系统分布在不同机器上,这就使得原有的并发控制锁策略失效,为了解决这个问题就需要一种跨进程的互斥机制来控制共享资源的访问,这就需要用到分布式锁!

分布式锁的实现有多种方式,下面介绍下这几种分布式锁的实现方式:

  • 基于数据库实现分布式锁,(适用于并发小的系统);
  • 基于缓存(Redis等)实现分布式锁,(效率高,最流行,存在锁超时的问题);
  • 基于Zookeeper实现分布式锁,(可靠,但是效率不高);

尽管有这三种方案,但是不同的业务也要根据自己的情况进行选型,他们之间没有最好只有更适合!

五、基于Redis实现分布式锁

使用Redis实现分布式锁是目前比较流行的解决方案,主要是使用Redis 获取锁与释放锁效率都很高,实现方式也特别简单。

实现原理:

(1)获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为当前线程ID,通过此在释放锁的时候进行判断。

(2)获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。

(3)释放锁的时候,通过线程ID判断是不是该锁,若是该锁,则执行delete进行锁释放。

接下来我们就一步一步实现Redis 分布式锁。

第一步,创建Spring Boot项目,并引入相关依赖。

org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-data-redis org.springframework.boot spring-boot-starter org.springframework.boot spring-boot-starter-test test org.apache.commons commons-lang3 com.alibaba fastjson 1.2.72 org.projectlombok lombok

第二步,创建Redis分布式锁通用操作类,示例代码如下:

import com.alibaba.fastjson.JSON;import lombok.extern.slf4j.Slf4j;import org.apache.commons.lang3.StringUtils;import org.springframework.context.annotation.Bean;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.data.redis.core.script.DefaultRedisScript;import org.springframework.scheduling.annotation.Async;import org.springframework.scheduling.annotation.Scheduled;import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;import org.springframework.stereotype.Component;import javax.annotation.Resource;import java.util.ArrayList;import java.util.List;import java.util.Map;import java.util.concurrent.ConcurrentHashMap;import java.util.concurrent.Executor;import java.util.concurrent.ThreadPoolExecutor;import java.util.concurrent.TimeUnit;/** * Lua脚本 * // 加锁 * if * redis.call(‘setNx’,KEYS[1],ARGV[1]) * then * if redis.call(‘get’,KEYS[1])==ARGV[1] * return redis.call(‘expire’,KEYS[1],ARGV[2]) * else * return 0 * end * end * * // 解锁 * redis.call(‘get’, KEYS[1]) == ARGV[1] * then * return redis.call(‘del’, KEYS[1]) * else * return 0 * * * //更新时间 * if redis.call(‘get’, KEYS[1]) == ARGV[1] then return redis.call(‘expire’, KEYS[1], ARGV[2]) else return 0 end * * * * */@Slf4j@Componentpublic class RedisLockUtils { @Resource private RedisTemplate redisTemplate; private static Map lockInfoMap = new ConcurrentHashMap(); private static final Long SUCCESS = 1L; @Data public static class LockInfo { private String key; private String value; private int expireTime; //更新时间 private long renewalTime; //更新间隔 private long renewalInterval; public static LockInfo getLockInfo(String key, String value, int expireTime) { LockInfo lockInfo = new LockInfo(); lockInfo.setKey(key); lockInfo.setValue(value); lockInfo.setExpireTime(expireTime); lockInfo.setRenewalTime(System.currentTimeMillis()); lockInfo.setRenewalInterval(expireTime * 2000 / 3); return lockInfo; } } /** * 使用lua脚本加锁 * @param lockKey 锁 * @param value 身份标识(保证锁不会被其他人释放) * @param expireTime 锁的过期时间(单位:秒) * @Desc 注意事项,redisConfig配置里面必须使用 genericToStringSerializer序列化,否则获取不了返回值 */ public boolean tryLock(String lockKey, String value, int expireTime) { String luaScript = “if redis.call(‘setNx’,KEYS[1],ARGV[1]) then if redis.call(‘get’,KEYS[1])==ARGV[1] then return redis.call(‘expire’,KEYS[1],ARGV[2]) else return 0 end end”; DefaultRedisScript redisScript = new DefaultRedisScript(); redisScript.setResultType(Boolean.class); redisScript.setScriptText(luaScript); List keys = new ArrayList(); keys.add(lockKey); //Object result = redisTemplate.execute(redisScript, Collections.singletonList(lockKey),value,expireTime + “”); // Object result = redisTemplate.execute(redisScript, new StringRedisSerializer(), new StringRedisSerializer(), Collections.singletonList(lockKey), identity, expireTime); Object result = redisTemplate.execute(redisScript, keys, value, expireTime); log.info(“已获取到{}对应的锁!”, lockKey); if (expireTime >= 10) { lockInfoMap.put(lockKey + value, LockInfo.getLockInfo(lockKey, value, expireTime)); } return (boolean) result; } /** * 使用lua脚本释放锁 * @param lockKey * @param value * @return 成功返回true, 失败返回false */ public boolean unlock(String lockKey, String value) { lockInfoMap.remove(lockKey + value); String luaScript = “if redis.call(‘get’, KEYS[1]) == ARGV[1] then return redis.call(‘del’, KEYS[1]) else return 0 end”; DefaultRedisScript redisScript = new DefaultRedisScript(); redisScript.setResultType(Boolean.class); redisScript.setScriptText(luaScript); List keys = new ArrayList(); keys.add(lockKey); Object result = redisTemplate.execute(redisScript, keys, value); log.info(“解锁成功:{}”, result); return (boolean) result; } /** * 使用lua脚本更新redis锁的过期时间 * @param lockKey * @param value * @return 成功返回true, 失败返回false */ public boolean renewal(String lockKey, String value, int expireTime) { String luaScript = “if redis.call(‘get’, KEYS[1]) == ARGV[1] then return redis.call(‘expire’, KEYS[1], ARGV[2]) else return 0 end”; DefaultRedisScript redisScript = new DefaultRedisScript(); redisScript.setResultType(Boolean.class); redisScript.setScriptText(luaScript); List keys = new ArrayList(); keys.add(lockKey); Object result = redisTemplate.execute(redisScript, keys, value, expireTime); log.info(“更新redis锁的过期时间:{}”, result); return (boolean) result; } /** * * @param lockKey 锁 * @param value 身份标识(保证锁不会被其他人释放) * @param expireTime 锁的过期时间(单位:秒) * @return 成功返回true, 失败返回false */ public boolean lock(String lockKey, String value, long expireTime) { return redisTemplate.opsForValue().setIfAbsent(lockKey, value, expireTime, TimeUnit.SECONDS); } /** * redisTemplate解锁 * @param key * @param value * @return 成功返回true, 失败返回false */ public boolean unlock2(String key, String value) { Object currentValue = redisTemplate.opsForValue().get(key); boolean result = false; if (StringUtils.isNotEmpty(String.valueOf(currentValue)) && currentValue.equals(value)) { result = redisTemplate.opsForValue().getOperations().delete(key); } return result; } /** * 定时去检查redis锁的过期时间 */ @Scheduled(fixedRate = 5000L) @Async(“redisExecutor”) public void renewal() { long now = System.currentTimeMillis(); for (Map.Entry lockInfoEntry : lockInfoMap.entrySet()) { LockInfo lockInfo = lockInfoEntry.getValue(); if (lockInfo.getRenewalTime() + lockInfo.getRenewalInterval() < now) { renewal(lockInfo.getKey(), lockInfo.getValue(), lockInfo.getExpireTime()); lockInfo.setRenewalTime(now); log.info("lockInfo {}", JSON.toJSONString(lockInfo)); } } } /** * 分布式锁设置单独线程池 * @return */ @Bean("redisExecutor") public Executor redisExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(1); executor.setMaxPoolSize(1); executor.setQueueCapacity(1); executor.setKeepAliveSeconds(60); executor.setThreadNamePrefix("redis-renewal-"); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy()); return executor; }}

第三步,创建RedisTemplate 配置类,配置Redistemplate,示例代码如下:

@Configurationpublic class RedisConfig { @Bean public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) throws Exception { RedisTemplate redisTemplate = new RedisTemplate(); redisTemplate.setConnectionFactory(redisConnectionFactory); // 创建 序列化类 GenericToStringSerializer genericToStringSerializer = new GenericToStringSerializer(Object.class); redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(genericToStringSerializer); return redisTemplate; }}

第四步,实现业务调用,这里以扣减库存为例,示例代码如下:

@RestControllerpublic class IndexController { @Resource private RedisTemplate redisTemplate; @Autowired private RedisLockUtils redisLock; @RequestMapping(“/deduct-stock”) public String deductStock() { String productId = “product001”; System.out.println(“—————->>>开始扣减库存”); String key = productId; String requestId = productId + Thread.currentThread().getId(); try { boolean locked = redisLock.lock(key, requestId, 10); if (!locked) { return “error”; } //执行业务逻辑 //System.out.println(“—————->>>执行业务逻辑:”+appTitle); int stock = Integer.parseInt(redisTemplate.opsForValue().get(“product001-stock”).toString()); int currentStock = stock-1; redisTemplate.opsForValue().set(“product001-stock”,currentStock); try { Random random = new Random(); Thread.sleep(random.nextInt(3) *1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(“—————->>>扣减库存结束:current stock:” + currentStock); return “success,current stock:” + currentStock; } finally { redisLock.unlock2(key, requestId); } }}

六、验证测试

代码完成之后,开始测试。我们同时启动两个实例,端口号为:8888和8889模拟分布式系统。

接下来,我们分别请求:http://localhost:8888/deduct-stock和http://localhost:8889/deduct-stock,或者使用JMater分别请求这两个地址,模拟高并发的情况。

通过上图我们可以看到,在批量请求的情况下,库存扣减也没有出现问题。说明分布式锁生效了。

最后

以上,我们就把什么是分布式锁,如何基于Redis 实现分布式锁的解决方案介绍完了。分布式锁是分布式系统中的重要功能组件,希望大家能够熟练掌握。

郑重声明:本文内容及图片均整理自互联网,不代表本站立场,版权归原作者所有,如有侵权请联系管理员(admin#wlmqw.com)删除。
(0)
用户投稿
上一篇 2022年6月12日
下一篇 2022年6月12日

相关推荐

  • 如何养成良好的排便习惯?首先要这样做……

    良好的排便习惯为每日定时排便一次,排便顺畅,便后有轻松感。生活中养成这种排便习惯,不仅可有助于排出体内的毒素和废弃物质,清洁肠道,避免便秘,还能促进胃肠道蠕动,减轻胃肠道负担,有效…

    2022年8月14日
  • 天文学家在太空爆发中捕捉到新流星雨

    虽然太空是一个空旷的真空,但它同时也是一个非常有活力的地方,那里有大量的天体一直穿过内太阳系并搅动着一切。有时,甚至可以在地球上看到宇宙邻居在被称之为大气层的安全毯下的变化。 上周…

    2022年8月25日
  • 《千与千寻》潜在台词的意义,你看懂了吗

    千与千寻是看过最多的电影之一,只要好看就总喜欢反复的去看,孤独的时候,无聊的时候看一遍,今天我们来说一说千与千寻中那些反复看、长大了之后才能明白的道理。 1、千寻一家人穿过隧道后可…

    2022年5月10日
  • switch和ps5怎么选择有什么区别 switch和ps5哪个好玩游戏多

    当你想拥有一个游戏主机的时候,恭喜你,你即将踏入游戏的新世界,主机游戏可比手机游戏好玩多了。不过此时此刻,你就面临一个至关重要的选择了,那就是选PS5还是Switch?哪个更适合自…

    2022年9月22日
  • 4款神仙PC软件,你的电脑上怎么能少了它们?

    虽说现在是移动端当道,但是真正工作的时候,还是PC端更占优势,也更加给力。今天分享4款PC端的神仙软件,如果你的电脑上还没有它们,建议安装起来哟。 01 Eagle eagle.c…

    2022年7月2日
  • 最新的日常美好生活文案,发朋友圈人人点赞

    一、恰到好处的喜欢最舒服,懂分寸的关系最迷人。 二、我活在世上,无非想要明白些道理,遇见些有趣的事,倘能如我愿,我的一生就算成功。 三、别活的跟支烟似的,无聊时让人点起你,抽完了又…

    2022年8月2日
  • 你以为穿身旗袍就是中国风,太落伍了!玩转“新中式风”才够时髦

    东方之美是含蓄、儒雅、温婉的体现,如今越来越多的女性开始追求代表着东方美的中国风,所以便出现了各种各样的旗袍造型,可是你以为穿身旗袍就是中国风了吗?如果你还有这种想法,那就太落伍了…

    2022年8月18日
  • 新农合什么时候开始缴费 大多数地区是这样安排的

    新农合的全称新型农村合作医疗,据说今年保费上涨了,但为了减轻看病贵和看病难等问题,大家按时缴费最安全,那么新农合什么时候开始缴费呢?下面来看规定。 2022年各地区新农合缴费时间不…

    2022年9月2日
  • 意念与思念的分析

    在《慎独与诚意》一文中,选取选取“意念与思念”与君共享。 意念是个人假定对象臆造所想之事。特点是以心为出发点,选择对象,加以浮想锻造。通常所说意志力不坚定的人,说的就是意念,在做事…

    2022年4月30日
  • 大同疫情解封最新消息:静默管理多久解除(疫情最新解封消息)

    山西大同疫情目前的新增病例还比较多,因此,大同此前就发布了继续在平城区、云冈区、新荣区、云州区“四区”实施静默管理的通告,时间延至10月18日6时。那么,大同现在解除静默管理了吗?…

    2022年10月18日

联系我们

联系邮箱:admin#wlmqw.com
工作时间:周一至周五,10:30-18:30,节假日休息