Redis实现分布式锁

分布式锁介绍

在分布式系统中,系统中存在不同的应用,并且不同的应用部署在不同的服务器中,分布式锁简单来说就是控制分布式系统中不同应用(进程)之间访问共享资源的一种锁的实现

分布式锁常用使用场景

分布式锁在电商下单,秒杀、多人抢购、大促活动等场景下应用广泛。

多个服务间 + 保证同一时刻内 + 同一用户只能有一个请求(防止关键业务出现数据冲突和并发错误)

分布式锁的实现

实现分布式锁有三种方式,如下列表所示:

  1. 使用Zookeeper实现分布式锁。
  2. 使用MySQL实现分布式锁。
  3. 使用Redis实现分布式锁。

本文对Redis分布式锁演进进行简单介绍。

Redis分布式锁常见面试题

  • Redis除了拿来做缓存,你还见过基于Redis的什么用法?

  • Redis做分布式锁的时候有需要注意的问题?

  • 如果是Redis是单点部署的,会带来什么问题?那你准备怎么解决单点问题呢?

  • 集群模式下,比如主从模式,有没有什么问题呢?

  • 那你简单的介绍一下Redlock吧?你简历上写redisson,你谈谈。

  • Redis分布式锁如何续期?看门狗知道吗?

前期准备

搭建库存超卖程序Demo

使用IDEA搭建连个SpringBoot项目,spring-boot-redis-lock01、spring-boot-redis-lock02,先建Module模块lock01,配置完成后复制为Module模块lock02即可。具体过程忽略,可按如下步骤创建:

  1. 建Module
  2. 改Pom (Maven配置文件)
  3. 写yml(项目配置文件)
  4. 主启动
  5. 业务类
  6. 小测试

Pom文件


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0modelVersion>
    <parent>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-parentartifactId>
        <version>2.3.3.RELEASEversion>
        <relativePath/> 
    parent>

    <groupId>com.redis.lockgroupId>
    <artifactId>spring-boot-redis-lock01artifactId>
    <version>0.0.1-SNAPSHOTversion>
    <name>spring-boot-redis-lock01name>
    <description>spring-boot-redis-lock01description>

    <properties>
        <java.version>1.8java.version>
    properties>

    <dependencies>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-actuatorartifactId>
        dependency>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-data-redisartifactId>
        dependency>
        <dependency>
            <groupId>org.apache.commonsgroupId>
            <artifactId>commons-pool2artifactId>
        dependency>
        
        <dependency>
            <groupId>redis.clientsgroupId>
            <artifactId>jedisartifactId>
            <version>3.1.0version>
        dependency>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-aopartifactId>
        dependency>
        
        <dependency>
            <groupId>org.redissongroupId>
            <artifactId>redissonartifactId>
            <version>3.13.4version>
        dependency>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-devtoolsartifactId>
            <scope>runtimescope>
            <optional>trueoptional>
        dependency>
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <optional>trueoptional>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId><scope>testscope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintagegroupId>
                    <artifactId>junit-vintage-engineartifactId>
                exclusion>
            exclusions>
        dependency>
    dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-maven-pluginartifactId>
            plugin>
        plugins>
    build>
project>

application.properties文件

server.port=1111
#Module02端口为2222

#=========================redis相关配置========================
#Redis数据库索引(默认方0)
spring.redis.database=0
#Redis服务器地址
spring.redis.host=127.0.0.1
#Redis服务器连接端口
spring.redis.port=6379
#Redis服务器连接密码(默认为空)
spring.redis.password=123admin
#连接池最大连接数(使用负值表示没有限制)默认8
spring.redis.lettuce.pool.max-active=8
#连接池最大阻塞等待时间(使用负值表示没有限制)默认-1
spring.redis.lettuce.pool.max-wait=-1
#连接池中的最大空闲连接默认8
spring.redis.lettuce.pool.max-idle=8
#连接池中的最小空闲连接默犬认0
spring.redis.lettuce.pool.min-idle=0

启动类

package com.redis.lock;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringBootRedisLock01Application {

    public static void main(String[] args) {
        SpringApplication.run(SpringBootRedisLock01Application.class, args);
    }
}

配置类

这里配置Redis的相关配置类

package com.redis.lock.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.io.Serializable;
import java.net.UnknownHostException;

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Serializable> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) throws UnknownHostException {
        RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(lettuceConnectionFactory);
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        return redisTemplate;
    }
}

业务类

因为是演示Demo,为了简单起见,直接在Controller层写业务代码。

@RestController
public class GoodController{


	private final Lock lock = new ReentrantLock();
	
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Value("${server.port}")
    private String serverPort;

    @GetMapping("/buy_goods")
    public String buyGoods(){
    	// get key 看看库存的数量够不够
        String result = stringRedisTemplate.opsForValue().get("goods:001");
        int goodsNumber = result == null ? 0 : Integer.parseInt(result);
        if(goodsNumber > 0){
            int realNumber = goodsNumber - 1;
            stringRedisTemplate.opsForValue().set("goods:001", String.valueOf(realNumber));
            System.out.println("成功买到商品,库存还剩下: "+ realNumber + " 件" + "\t服务提供端口" + serverPort);
            return "成功买到商品,库存还剩下:" + realNumber + " 件" + "\t服务提供端口" + serverPort;
        }else{
            System.out.println("商品已经售完/活动结束/调用超时,欢迎下次光临" + "\t服务提供端口" + serverPort);
        }
        return "商品已经售完/活动结束/调用超时,欢迎下次光临" + "\t服务提供端口" + serverPort;
    }
}

小测试

首先在本地Redis中设置商品库存数量:

127.0.0.1:6379> set goods:001 100
OK
127.0.0.1:6379> 

然后启动项目,在浏览器中访问如下地址:

http://localhost:1111/buy_goods
返回数据:成功买到商品,库存还剩下:99 件 服务提供端口1111

测试成功后,将spring-boot-redis-lock01 复制为spring-boot-redis-lock02,把端口改为2222即可。至此,准备工作完成。

Redis分布式锁演进过程

版本01

/**
     * 版本1,不加锁,在单机、非高平发下可以运行正常。但是在多线程、高并发下就会出现问题。
     * @return
     */
    private String version01(){
        // get key ====看看库存的数量够不够
        //获取值和对值修改都是非原子性操作
        String result = stringRedisTemplate.opsForValue().get("goods:001");
        int goodsNumber = result == null ? 0 : Integer.parseInt(result);
        if(goodsNumber > 0){
            int realNumber = goodsNumber - 1;
            stringRedisTemplate.opsForValue().set("goods:001", String.valueOf(realNumber));
            System.out.println("成功买到商品,库存还剩下: "+ realNumber + " 件" + "\t服务提供端口" + serverPort);
            return "成功买到商品,库存还剩下:" + realNumber + " 件" + "\t服务提供端口" + serverPort;
        }else{
            System.out.println("商品已经售完/活动结束/调用超时,欢迎下次光临" + "\t服务提供端口" + serverPort);
        }
        return "商品已经售完/活动结束/调用超时,欢迎下次光临" + "\t服务提供端口" + serverPort;
    }

版本1在单机、串行访问下可以运行正常,但是在多线程、高并发下就会出现库存超卖的问题。

对版本1 进行修改,加锁。

版本2

在版本1的基础上增加JVM层面的锁,如synchronized、ReentrantLock,版本2如下:

/**
     * 版本2,单机版synchronized锁
     * @return
     */
    private String synchronizedVersion02() {
        synchronized (this){
            /**
            * 方法体,均为版本1代码
            */
        }
    }
    /**
     * 版本2,单机版Lock
     * 单机锁没办法解决分布式部署下超卖问题。
     * @return
     */
    private String lockVersion02() {
        try {
        	//lock定义请查看“业务类”模块
        	//lock.lock();block until condition holds 不见不散
            lock.tryLock(2, TimeUnit.SECONDS);//过时不候,超过等待时间就不再等待
            /**
            *
            * 方法体,均为版本1代码
            */
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
        return "商品已经售完/活动结束/调用超时,欢迎下次光临" + "\t服务提供端口" + serverPort;
    }

版本2加完单机锁后,在单机环境、多线程情况下,也可以正常访问。
但是在分布式环境部署情况下,还是会出现超卖情况。

版本3

对Demo进行分布式改造。架构如下所示:
Redis实现分布式锁_第1张图片web01代表serverLock01,web02代表serverLock02
分布式部署以后,单机锁还是出现超卖现象,需要分布式锁
演示需要使用Nginx,Nginx安装可以参考本人的这篇文章Nginx简单介绍和详细安装。此处不在赘述。

修改Nginx配置文件

修改Nginx的配置文件,进行负均衡配置

#业务服务地址
upstream redisLock{
    server 127.0.0.1:1111;
    server 127.0.0.1:2222;
}

server {
    listen       80;
    server_name  localhost;
    location / {
        # 用到的负载均衡配置
        proxy_pass  http://redisLock;
        root   html;
        index  index.html index.htm;
    }
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
    	root   html;
    }
}

配置完成后重新启动Nginx使配置生效:

nginx -s reload

启动1111、2222两个后端服务,在浏览器中访问http://localhost/buy_goods 进行手动刷新,可以看到服务在端口1111,2222两者之间轮流出现(Nginx默认的负载均衡策略为轮训)。

// 服务端口1111返回数据
成功买到商品,库存还剩下: 95 件	服务提供端口1111
成功买到商品,库存还剩下: 93 件	服务提供端口1111
成功买到商品,库存还剩下: 91 件	服务提供端口1111
成功买到商品,库存还剩下: 89 件	服务提供端口1111

//服务端口2222返回数据
成功买到商品,库存还剩下: 96 件	服务提供端口2222
成功买到商品,库存还剩下: 94 件	服务提供端口2222
成功买到商品,库存还剩下: 92 件	服务提供端口2222
成功买到商品,库存还剩下: 90 件	服务提供端口2222

Apach Jmeter压测,模拟高并发

上边通过手动刷新浏览器,下面开始使用Jmeter模拟高并发请求,观察数据输出,看是否会出现问题!

恢复Redis库存数据:

127.0.0.1:6379> get goods:001
"75"
127.0.0.1:6379> set goods:001 100
OK
127.0.0.1:6379> 

打开Jmeter,配置100个线程在1秒内执行,访问http://localhost/buy_goods
Redis实现分布式锁_第2张图片
Redis实现分布式锁_第3张图片
启动测试,查看打印的日志信息:

Redis实现分布式锁_第4张图片
Redis实现分布式锁_第5张图片
如上日志可以看出,进行分布式改造后出现了超卖现象。

结论:JVM锁(单机锁)在分布式架构下已不再适用,必须改用分布式锁。

Redis具有极高的性能,且其命令对分布式锁支持友好,借助SET命令即可实现加锁处理。

SET命令

  • EX seconds – Set the specified expire time, in seconds.
  • PX milliseconds – Set the specified expire time, in milliseconds.
  • NX – Only set the key if it does not already exist.
  • XX – Only set the key if it already exist.

中文解释:

  • EX seconds – 设置键key的过期时间,单位是秒
  • PX milliseconds – 设置键key的过期时间,单位是毫秒
  • NX – 只有键key不存在的时候才会设置key的值
  • XX – 只有键key存在的时候才会设置key的值

版本3代码如下:

	/**
     * 定义RedisLock锁常量
     */
    private static final String REDIS_LOCK = "redisLock";
    /**
     * 版本3,使用Redis的setnx命令作为分布式锁
     * @return
     */
    private String redisSetNxCommand03() {
        String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
        //setnx
        boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value);
        if (!flag){
            return "抢锁失败";
        }
       /**
       * 方法体,均为版本1代码
       */
       //解锁操作
       stringRedisTemplate.delete(REDIS_LOCK);
    }

版本04

版本3中存在问题,加锁后,程序在执行过程中,如果出现异常退出,将会导致锁没有被释放。对三版本进行改造,加finally块,在finally块中进行解锁。如下:

private String redisSetNxCommandFinally04() {
        String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
        try {
            //setnx
            boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value);
            if (!flag){
                return "抢锁失败";
            }
            /**
            * 方法体,均为版本1 代码
            */
        }finally {
            //解锁
            stringRedisTemplate.delete(REDIS_LOCK);
        }
    }

版本5

版本4中的程序同样存在一定问题,在加了finally块后,虽然可以保证程序在正常、异常情况下都能解锁,但是考虑一些极端情况,比如服务器宕机(部署了微服务jar包的机器挂了),代码层面是无法运行到finally块的,这样就不能保证锁被释放,key就无法被删除。
解决:在加锁时设置过期时间。
版本5代码如下:

private String redisSetNxCommandExpire05() {
        String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
        try {
            //setnx
            boolean flag = Boolean.TRUE.equals(stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value));
            //设置过期时间(非原子操作)
            stringRedisTemplate.expire(REDIS_LOCK,10,TimeUnit.SECONDS);
            if (!flag){
                return "抢锁失败";
            }
            /**
            * 方法体,均为版本1 代码
            */
        }finally {
            //解锁
            stringRedisTemplate.delete(REDIS_LOCK);
        }
    }

版本6

版本5中依然存在问题,那就是加锁操作和设置锁过期时间的操作是两步,并非原子操作。将其改为原子操作,版本6代码如下:

private String redisSetNxCommandExpire06() {
        String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
        try {
        	//原子操作,设置锁的同时,设置过期时间
            Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value, 10, TimeUnit.SECONDS);
            if (!flag){
                return "抢锁失败";
            }
			/**
            * 方法体,均为版本1 代码
            */
        }finally {
            //解锁
            stringRedisTemplate.delete(REDIS_LOCK);
        }

版本7

版本6中还是存在问题,如果业务执行时间超时,锁自动过期,当业务执行完进行解锁操作时,有可能会把其他线程的锁删除掉。
因此,在解锁前必须进行判断,如果是自己的锁,就删除,不是自己的锁就不能删除。
版本7代码如下:

private String redisSetNxCommandExpire07() {
        String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
        try {
        	//原子操作,设置锁的同时,设置过期时间
            Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value, 10, TimeUnit.SECONDS);
            if (!flag){
                return "抢锁失败";
            }
            /**
            * 方法体,均为版本1 代码
            */
        }finally {
            //解锁需进行判断:判断加锁与解锁有可能不是同一客户端。(非原子操作)
            if (stringRedisTemplate.opsForValue().get(REDIS_LOCK).equals(value)){
                //若在此时,这把锁突然不是这个客户端的,则会误解锁。
                stringRedisTemplate.delete(REDIS_LOCK);
            }
        }
    }

版本8

版本7看起来已经很不错了,但是还是存在问题,那就是解锁判断和解锁操作不是原子操作,类似于加锁操作和设置过期时间不是原子操作一样,判断加锁与解锁有可能不是同一客户端,可能会出现误解锁。
解锁操作同样也必须是原子操作,使用Redis+lua脚本进行解锁。
增加RedisUtils工具类

package com.redis.lock.utils;

/**
 * @ClassName RedisUtils
 * @Description TODO
 * @Version 1.0
 **/
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

public class RedisUtils {

    private static JedisPool jedisPool;

    static {
        JedisPoolConfig jpc = new JedisPoolConfig();
        jpc.setMaxTotal(20);
        jpc.setMaxIdle(10);
        jedisPool = new JedisPool(jpc);
    }

    public static Jedis getJedis() throws Exception{
        if(jedisPool == null) {
            throw new NullPointerException("JedisPool is not OK.");
        }
        return jedisPool.getResource();
    }
}

版本8 lua脚本代码示例如下:

private String redisSetNxCommandLua081() throws Exception {
        String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
        try {
            //原子加锁并设置过期时间
            Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value, 10, TimeUnit.SECONDS);
            if (!flag){
                return "抢锁失败";
            }
           /**
            * 方法体,均为版本1 代码
            */
        }finally {
            //lua脚本方法1 (官网方式)
            String luaScript = "if redis.call(\"get\",KEYS[1]) == ARGV[1]" +
                    "then" +
                    "    return redis.call(\"del\",KEYS[1])\n" +
                    "else" +
                    "    return 0" +
                    "end";
            //使用lua脚删除锁
            Long lock1 = stringRedisTemplate.execute(new DefaultRedisScript<Long>(luaScript, Long.class),
                    Arrays.asList(REDIS_LOCK), value);
            
            //lua脚本方法2
            //使用jedis
            Jedis jedis = RedisUtils.getJedis();
            String script = "if redis.call('get', KEYS[1]) == ARGV[1] "
                    + "then "
                    + "    return redis.call('del', KEYS[1]) "
                    + "else "
                    + "    return 0 "
                    + "end";
            try {
                Object o = jedis.eval(script, Collections.singletonList(REDIS_LOCK),
                        Collections.singletonList(value));
                if("1".equals(o.toString())) {
                    System.out.println("---del redis lock ok.");
                }else {
                    System.out.println("---del redis lock error.");
                }
            }finally {
                if(jedis != null){
                    jedis.close();
                }
            }
        }

那有人就问了,如果不使用lua脚本,是否还有其他方法???
必须有,可以使用Redis自身的事务进行解决!
Redis事务

版本8 使用Redis事务进行解锁操作如下:

private String redisSetNxCommandTran081() {
        String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
        try {
            //原子加锁并设置过期时间
            Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value, 10, TimeUnit.SECONDS);
            if (!flag){
                return "抢锁失败";
            }
            /**
            * 方法体,均为版本1 代码
            */
            
        }finally {
            /**
             * 问:如果此处不使用lua脚本,是否还有其他方法???
             * 用Redis自身的事务来处理。
             */
            while (true){
                stringRedisTemplate.watch(REDIS_LOCK);
                if (stringRedisTemplate.opsForValue().get(REDIS_LOCK).equals(value)){
                    stringRedisTemplate.setEnableTransactionSupport(true);
                    stringRedisTemplate.multi();
                    stringRedisTemplate.delete(REDIS_LOCK);
                    List<Object> list = stringRedisTemplate.exec();
                    if (list.isEmpty()){
                        continue;
                    }
                }
                stringRedisTemplate.unwatch();
                break;
            }
        }

总结:分布式锁必须满足原子加锁和原子解锁

在版本8两种情况下,需要确保redisLock过期时间大于业务执行时间。(考虑Redis分布式锁如何实现缓存续期?看门狗模式)

我们自己看门狗缓存续期,会很复杂。在集群模式下我们自己实现Redis锁也不是很OK,直接使用RedLock的落地实现Redisson来实现Redis的分布式锁。
Redis分布式锁&RedLock算法

版本9-使用Redisson

使用Redisson作为分布式锁:
在POM中加入Redisson的依赖


        <dependency>
            <groupId>org.redissongroupId>
            <artifactId>redissonartifactId>
            <version>3.13.4version>
        dependency>

在RedisConfig配置类中增加Redisson配置

	/**
     * 单机Redisson
     * @return
     */
    @Bean
    public Redisson redisson() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379").setDatabase(0);
        return (Redisson) Redisson.create(config);
    }

代码层面直接注入Redisson即可

	@Autowired
    private Redisson redisson;
    
	private String redissonVersion9() {
		//获取分布式锁
        RLock redissonLock = redisson.getLock(REDIS_LOCK);
        redissonLock.lock();
        try {
            String result = stringRedisTemplate.opsForValue().get("goods:001");
            int goodsNumber = result == null ? 0 : Integer.parseInt(result);
            if(goodsNumber > 0){
                int realNumber = goodsNumber - 1;
                stringRedisTemplate.opsForValue().set("goods:001", String.valueOf(realNumber));
                System.out.println("成功买到商品,库存还剩下: "+ realNumber + " 件" + "\t服务提供端口" + serverPort);
                return "成功买到商品,库存还剩下:" + realNumber + " 件" + "\t服务提供端口" + serverPort;
            }else{
                System.out.println("商品已经售完/活动结束/调用超时,欢迎下次光临" + "\t服务提供端口" + serverPort);
            }
            return "商品已经售完/活动结束/调用超时,欢迎下次光临" + "\t服务提供端口" + serverPort;
        }finally {
        	//解锁
            redissonLock.unlock();
        }
    }

该情况下可以说已经接近完美了,但是在极高的并发下,在finally块中直接使用 redissonLock.unlock()解锁, 还是会有几率产生如下异常:

IllegalMonitorStateException: attempt to unlock lock,not loked by current thread by node id:da6385f-81a5-4e6c-b8c0

对版本9中的解锁代码进行优化。

版本10

对解锁逻辑进行优化,最终完整版如下:

package com.redis.lock.controller;

import com.redis.lock.utils.RedisUtils;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import redis.clients.jedis.Jedis;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @ClassName GoodsController
 * @Version 1.0
 **/
@RestController
public class GoodsController {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Value("${server.port}")
    private String serverPort;

    /**
     * 定义RedisLock锁常量
     */
    private static final String REDIS_LOCK = "redisLock";


    @Autowired
    private Redisson redisson;

    @GetMapping("/buy_goods")
    public String buy_Goods(){
        RLock redissonLock = redisson.getLock(REDIS_LOCK);
        redissonLock.lock();
        try {
            String result = stringRedisTemplate.opsForValue().get("goods:001");
            int goodsNumber = result == null ? 0 : Integer.parseInt(result);
            if (goodsNumber > 0) {
                int realNumber = goodsNumber - 1;
                stringRedisTemplate.opsForValue().set("goods:001", String.valueOf(realNumber));
                System.out.println("成功买到商品,库存还剩下: " + realNumber + " 件" + "\t服务提供端口" + serverPort);
                return "成功买到商品,库存还剩下:" + realNumber + " 件" + "\t服务提供端口" + serverPort;
            } else {
                System.out.println("商品已经售完/活动结束/调用超时,欢迎下次光临" + "\t服务提供端口" + serverPort);
            }
            return "商品已经售完/活动结束/调用超时,欢迎下次光临" + "\t服务提供端口" + serverPort;
        } finally {
            //解锁推荐使用此种写法
            if (redissonLock.isLocked()) {
                if (redissonLock.isHeldByCurrentThread()) {
                    redissonLock.unlock();
                }
            }
        }
    }
}

Redis分布式锁总结

  • ReentrantLock、synchronized Jvm锁在单机版上是可用的。

  • nginx分布式微服务单机锁不行。

  • 取消单机锁,上Redis分布式锁,使用setnx命令
    只加了锁,没有释放锁,出异常的话,可能无法释放锁,必须要在代码层面finally释放锁。
    宕机了,部署了微服务代码层面根本没有走到finally代码块,没办法保证解锁,这个key没有被删除,需要有lockKey的过期时间设定。

  • 为redis的分布式锁key,增加过期时间,此外,还必须要setnx+过期时间必须同一行,即保证原子操作。

  • 必须规定只能自己删除自己的锁,不能把别人的锁删除了,防止张冠李戴,1删2,2删3。

  • Redis集群环境下,我们自己写的也不oK直接上RedLock之Redisson落地实现。

你可能感兴趣的:(Redis,redis,分布式,java)