首页 > 编程语言 > SpringBoot使用Redis实现分布式锁
2020
09-29

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;
  }
}

结语

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

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

编程技巧