SpringBoot使用Redis实现分布式锁

 更新时间:2020年05月16日 15:28:01   作者:WP七夕  
这篇文章主要为大家详细介绍了SpringBoot使用Redis实现分布式锁,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

前言

在单机应用时代,我们对一个共享的对象进行多线程访问的时候,使用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;
  }
}

结语

感谢大家阅读我的文章,更欢迎大家指出我的问题,希望能在这里通过讨论取得共同的进步。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

相关文章

  • Java并发之原子性 有序性 可见性及Happen Before原则

    Java并发之原子性 有序性 可见性及Happen Before原则

    一提到happens-before原则,就让人有点“丈二和尚摸不着头脑”。这个涵盖了整个JMM中可见性原则的规则,究竟如何理解,把我个人一些理解记录下来。下面可以和小编一起学习Java 并发四个原则
    2021-09-09
  • SpringBoot接口防抖(防重复提交)的实现方案

    SpringBoot接口防抖(防重复提交)的实现方案

    所谓防抖,一是防用户手抖,二是防网络抖动,在Web系统中,表单提交是一个非常常见的功能,如果不加控制,容易因为用户的误操作或网络延迟导致同一请求被发送多次,所以本文给大家介绍了SpringBoot接口防抖(防重复提交)的实现方案,需要的朋友可以参考下
    2024-04-04
  • java实现服务器文件打包zip并下载的示例(边打包边下载)

    java实现服务器文件打包zip并下载的示例(边打包边下载)

    这篇文章主要介绍了java实现服务器文件打包zip并下载的示例,使用该方法,可以即时打包文件,一边打包一边传输,不使用任何的缓存,让用户零等待,需要的朋友可以参考下
    2014-04-04
  • Springboot @Validated和@Valid的区别及使用详解

    Springboot @Validated和@Valid的区别及使用详解

    这篇文章主要介绍了Springboot @Validated和@Valid的区别及使用详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-05-05
  • 关于 Java 的数据结构链表

    关于 Java 的数据结构链表

    这篇文章主要介绍了关于 Java 的数据结构链表的相关资料,需要的朋友可以参考下面文章内容
    2021-09-09
  • Mybatis核心配置文件加载流程详解

    Mybatis核心配置文件加载流程详解

    本文将介绍MyBatis在配置文件加载的过程中,如何加载核心配置文件、如何解析映射文件中的SQL语句以及每条SQL语句如何与映射接口的方法进行关联,具有一定的参考价值,感兴趣的可以了解一下
    2023-12-12
  • 关于Java8中map()和flatMap()的一些事

    关于Java8中map()和flatMap()的一些事

    这篇文章主要给大家介绍了关于Java8中map()和flatMap()的一些事,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-10-10
  • Spring Boot 中的任务执行器基本概念及使用方法

    Spring Boot 中的任务执行器基本概念及使用方法

    务执行器是 Spring Boot 中的一个非常实用的模块,它可以简化异步任务的开发和管理,在本文中,我们介绍了任务执行器的基本概念和使用方法,以及一个完整的示例代码,需要的朋友可以参考下
    2023-07-07
  • Java中String的intern()方法详细说明

    Java中String的intern()方法详细说明

    这篇文章主要介绍了Java中String的intern()方法详细说明,String::intern()是一个本地方法,他的作用就是如果字符串常量池中已经包含了一个等于此String对象的字符串,则返回代表池中的这个字符串额String对象的引用,需要的朋友可以参考下
    2023-11-11
  • Mybatis通用Mapper(tk.mybatis)的使用

    Mybatis通用Mapper(tk.mybatis)的使用

    本文主要介绍了Mybatis通用Mapper(tk.mybatis)的使用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-07-07

最新评论