SpringBoot使用Redis实现分布式锁
作者:WP七夕 发布时间:2021-11-06 20:10:22
前言
在单机应用时代,我们对一个共享的对象进行多线程访问的时候,使用java的synchronized关键字或者ReentrantLock类对操作的对象加锁就可以解决对象的线程安全问题。
分布式应用时代这个方法却行不通了,我们的应用可能被部署到多台机器上,运行在不同的JVM里,一个对象可能同时存在多台机器的内存中,怎样使共享对象同时只被一个线程处理就成了一个问题。
在分布式系统中为了保证一个对象在高并发的情况下只能被一个线程使用,我们需要一种跨JVM的互斥机制来控制共享资源的访问,此时就需要用到我们的分布式锁了。
分布式锁一般有三种实现方式:1.通过数据库实现分布式锁;2.通过缓存(Redis等)实现分布式锁;3.通过Zookeeper实现分布式锁。本篇文章主要介绍第二种通过Redis实现分布式锁的方式。
分布式锁的需要具备的条件
为了保证分布式锁的可用性,需要具备一下五点条件:
1、在同一时间保证只有一台机器的一个线程可以持有锁。
2、不能发生死锁,无论何时持有锁的机器崩溃挂掉了都要能自动释放锁。
3、高效的获取和释放锁。
4、具备非阻塞性,一旦获取不到锁就立刻返回加锁失败。
5、独占性,即自己加的锁只有自己才能释放。
代码实现
组件依赖
首先在pom.xml文件中添加依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
加锁代码
代码如下:
/**
* 获取锁
* @param lockKey 锁
* @param identity 身份标识(保证锁不会被其他人释放)
* @param expireTime 锁的过期时间(单位:秒)
* @return
*/
public boolean lock(String lockKey, String identity, long expireTime){
boolean lockResult = redisTemplate.opsForValue().setIfAbsent(lockKey, identity, expireTime, TimeUnit.SECONDS);
return opsForValue;
}
加锁的方法只需要三个参数:lockKey、identity、expireTime。
第一个参数lockKey为key,一个资源对应一个唯一的key。
第二个参数identity为身份标识,作为此key对应的value存储,为了判断在释放锁时是不是和加锁的身份相同,防止别人释放锁。
第三个参数expireTime为过期时间,此参数保证程序加锁后崩溃导致不能主动释放锁的时候自动释放锁,防止出现死锁。
为什么使用setIfAbsent方法呢?这个方法的好处就是,如果redis中已经存在这个key了,就会返回失败,并且不改变redis中的数据,这样就不会把别的线程的加的锁给覆盖掉。
解锁代码
代码如下:
/**
* 释放锁
* @param lockKey 锁
* @param identity 身份标识(保证锁不会被其他人释放)
* @return
*/
public boolean releaseLock(String lockKey, String identity){
String luaScript =
"if " +
" redis.call('get', KEYS[1]) == ARGV[1] " +
"then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
DefaultRedisScript<Boolean> redisScript = new DefaultRedisScript<>();
redisScript.setResultType(Boolean.class);
redisScript.setScriptText(luaScript);
List<String> keys = new ArrayList<>();
keys.add(lockKey);
boolean result = redisTemplate.execute(redisScript, keys, identity);
return result;
}
解锁的方法只需两个参数:lockKey、identity。
第一个参数lockKey为key,一个资源对应一个唯一的key。
第二个参数identity为身份标识,作为此key对应的value存储,为了判断在释放锁时是不是和加锁的身份相同,防止别人释放锁。
此处使用Lua脚本来判断身份,身份相同就删除,身份不同就不对数据做操作并返回失败。为什么要使用Lua脚本呢?这是为了要保证操作的原子性,redis在执行Lua脚本的时候是把脚本当作一个命令来执行的,我们都知道redis的命令是都是原子操作,这样就保证了操作的原子性。
测试代码
package com.qixi.lock.demo.lockdemo.controller;
import com.qixi.lock.demo.lockdemo.util.RedisLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 测试分布式锁
* @author ZhengNC
* @date 2020/5/13 17:27
*/
@RestController
@RequestMapping("test")
public class TestRedisLockController {
private final String lockKeyName = "testKey";
@Autowired
private RedisLock redisLock;
/**
* 测试加锁
* @param id 加锁的资源id
* @param identity 身份标识
* @return
*/
@GetMapping("lock")
public String lock(@RequestParam("id") String id,
@RequestParam("identity") String identity){
String lockKey = lockKeyName+":"+id;
boolean lockSuccess = redisLock.lock(lockKey, identity, 60);
String result = "lock failed";
if (lockSuccess){
result = "lock success";
}
return result;
}
/**
* 测试释放锁
* @param id 释放锁的资源id
* @param identity 身份标识
* @return
*/
@GetMapping("release")
public String release(@RequestParam("id") String id,
@RequestParam("identity") String identity){
String lockKey = lockKeyName+":"+id;
boolean releaseSuccess = redisLock.releaseLock(lockKey, identity);
String result = "release failed";
if (releaseSuccess){
result = "release success";
}
return result;
}
}
package com.qixi.lock.demo.lockdemo.util;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* 分布式锁Redis工具类
* @author ZhengNC
* @date 2020/5/13 17:27
*/
@Component
public class RedisLock {
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* 获取锁
* @param lockKey 锁
* @param identity 身份标识(保证锁不会被其他人释放)
* @param expireTime 锁的过期时间(单位:秒)
* @return
*/
public boolean lock(String lockKey, String identity, long expireTime){
boolean lockResult = redisTemplate.opsForValue().setIfAbsent(lockKey, identity, expireTime, TimeUnit.SECONDS);
return lockResult;
}
/**
* 释放锁
* @param lockKey 锁
* @param identity 身份标识(保证锁不会被其他人释放)
* @return
*/
public boolean releaseLock(String lockKey, String identity){
String luaScript =
"if " +
" redis.call('get', KEYS[1]) == ARGV[1] " +
"then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
DefaultRedisScript<Boolean> redisScript = new DefaultRedisScript<>();
redisScript.setResultType(Boolean.class);
redisScript.setScriptText(luaScript);
List<String> keys = new ArrayList<>();
keys.add(lockKey);
boolean result = redisTemplate.execute(redisScript, keys, identity);
return result;
}
}
结语
感谢大家阅读我的文章,更欢迎大家指出我的问题,希望能在这里通过讨论取得共同的进步。
来源:https://blog.csdn.net/sinat_27245917/article/details/106119346


猜你喜欢
- 本文实例为大家分享了Android实现记事本功能的具体代码,供大家参考,具体内容如下MainActivity.java代码:package
- 1 简介Springboot是最简单的使用Spring的方式,而MongoDB是最流行的NoSQL数据库。两者在分布式、微服务架构中使用率极
- 泛型1、简单泛型泛型的主要目的之一就是用来指定容器要持有什么类型的对象,而且由编译器来保证类型的正确性。泛型暂时不指定类型,在使用时决定具体
- 需要用到 java 写一个 ftp 的工具,因为只有一点点 java 基础,但是由于好几年不用,几乎算是不会了,只好一点点来搞,还好能捡起来
- Spring中Profile对不同环境提供不同配置功能的支持,可以通过激活、指定参数等方式快速切换环境。文件名格式:application-
- 前言属于基础的面试问题,一定要能够回答全哦~一、继承Thread,重写run方法通过自定义一个类(这里起名为:MyThread),继承Thr
- Java语言中,把异常分为两类:受检异常: 这类异常必须在throws子句中被显式抛出或者在方法内被捕获。例如,IOException异常或
- 深拷贝是指源对象与拷贝对象互相独立,其中任何一个对象的改动都不会对另外一个对象
- 一、项目简述功能包括(管理员和游客角色):1:用户及主要操作功能 游客可以浏览网站的主页,但是需要注册为会员后部分小 说才能对网络小说进免费
- 一、项目简述功能包括:用户分为宠物,医生,管理员,宠物主人可进行注册选择医生挂号,选择日期,选择号源,医生可进行宠物接诊,管理员可对宠物,医
- maven报错:Failed to execute goal on project项目结构在hzz-entity项目上运行Maven ins
- 今天学习了Map集合的几种方法,尤其是遍历Map集合感觉尤为重要,所以发出来供大家学习和自己复习以用。众所周知Map集合里存储元素是以键值对
- 1.简介if判断语句是很多编程语言的重要组成部分。但是,若我们最终编写了大量嵌套的if语句,这将使得我们的代码更加复杂和难以维护。让我们看看
- C++ 11中的互斥量,声明在 <mutex> 头文件中,互斥量的使用可以在各种方面,比较常用在对共享数据的
- 1.创建线程 在Java中创建线程有两种方法:使用Thread类和使用Runnable接口。在使用Runnable接口时需要建立
- 工厂接口定义/// <summary> /// 工厂接口定义 &nbs
- 1.场景线程池使用DiscardOldestPolicy拒绝策略,阻塞队列使用ArrayBlockingQueue,发现在某些情形下对于得到
- 本文实例讲述了Java模拟QQ桌面截图功能实现方法。分享给大家供大家参考。具体如下:QQ的桌面截图功能非常方便,去年曾用Java模拟过一个,
- 为了实现自定义的Menu和ContextMenu效果,下面演示代码通过派生ProfessionalColorTable类,在自定义的类中重写
- Java的源代码是以*.java的纯文本文件,可以使用任何文本编辑器来进行编写,但是这个源代码是无法执行的。执行源代码的这个任务就需要JDK