Redis分布式锁

Redis分布式锁

为了防止分布式系统中的多个进程之间相互干扰,我们需要一种分布式协调技术来对这些进程进行调度。而这个分布式协调技术的核心就是来实现这个分布式锁

库存超卖

构建Redis分布式锁基础环境

maven依赖

<?xml version="1.0" encoding="UTF-8"?>
<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.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.2.13.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.chinda</groupId>
	<artifactId>redis-01</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>redis-01</name>
	<description>Redis project for Spring Boot</description>
	<properties>
		<java.version>1.8</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-actuator</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-redis</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-aop</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<scope>runtime</scope>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>redis.clients</groupId>
			<artifactId>jedis</artifactId>
		</dependency>
		<dependency>
			<groupId>org.redisson</groupId>
			<artifactId>redisson</artifactId>
			<version>3.13.4</version>
		</dependency>
		<dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-pool2</artifactId>
		</dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-core</artifactId>
            <version>5.2.1</version>
        </dependency>
    </dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<configuration>
					<excludes>
						<exclude>
							<groupId>org.projectlombok</groupId>
							<artifactId>lombok</artifactId>
						</exclude>
					</excludes>
				</configuration>
			</plugin>
		</plugins>
	</build>

</project>

资源配置

server.port=1111

# redis数据库索引(默认0)
spring.redis.database=0
# redis服务器地址
spring.redis.host=192.168.2.5
# redis服务器链接端接口
spring.redis.port=6379
# redis服务器链接密码(默认为空)
spring.redis.password=
# 连接池最大连接数[使用负值表示没有限制](默认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.mix-idle=0

Redis配置

package com.chinda.redis.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;

/**
 * @author Wang Chinda
 * @date 2021/4/13
 */
@Configuration
public class RedisConfig {

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

库存业务代码编写

不考虑并发情况1.0版本

package com.chinda.redis.controller;

import cn.hutool.core.convert.Convert;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author Wang Chinda
 * @date 2021/4/13
 */
@RestController
@RequestMapping("/product")
public class ProductController {

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

    @GetMapping
    public String getTestStr() {
        Integer result = Convert.toInt(redisTemplate.opsForValue().get("product:001"));

        if (result == null) {
            System.out.println("商品已经售完/活动结束/调用超时, 欢迎下次光临 " + "\t 服务提供端口: " + serverPort);
            return "商品已经售完/活动结束/调用超时, 欢迎下次光临 " + "\t 服务提供端口: " + serverPort;
        }

        int realNumber = result - 1;
        redisTemplate.opsForValue().set("product:001", Convert.toStr(realNumber));
        System.out.println("成功买到商品, 库存还剩下: " + realNumber + "件 \t 服务提供端口" + serverPort);
        return "成功买到商品, 库存还剩下: " + realNumber + "件 \t 服务提供端口" + serverPort;
    }
}

此时,在不考虑并发情况下,业务代码是没有任何问题的。若存在并发,此时就会出现超卖现象。

考虑并发,不考虑分布式情况2.0版本

package com.chinda.redis.controller;

import cn.hutool.core.convert.Convert;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author Wang Chinda
 * @date 2021/4/13
 */
@RestController
@RequestMapping("/product")
public class ProductController {

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

    @GetMapping
    public String getTestStr() {
        synchronized (this) {
            Integer result = Convert.toInt(redisTemplate.opsForValue().get("product:001"));

            if (result == null) {
                System.out.println("商品已经售完/活动结束/调用超时, 欢迎下次光临 " + "\t 服务提供端口: " + serverPort);
                return "商品已经售完/活动结束/调用超时, 欢迎下次光临 " + "\t 服务提供端口: " + serverPort;
            }

            int realNumber = result - 1;
            redisTemplate.opsForValue().set("product:001", Convert.toStr(realNumber));
            System.out.println("成功买到商品, 库存还剩下: " + realNumber + "件 \t 服务提供端口" + serverPort);
            return "成功买到商品, 库存还剩下: " + realNumber + "件 \t 服务提供端口" + serverPort;
        }
    }
}

此时会根据业务场景选择使用synchronized还是ReentrantLock。选择synchronized是一直阻塞状态,直到一线程释放锁下一线程才会获取锁。选择ReentrantLock可以使用boolean tryLock(long timeout, TimeUnit unit)方法设置超时时间,当锁超时时,会将锁自动释放,这时候会存在删错锁的情况,下文会出现Redis分布式锁类似的情况。

考虑分布式情况3.0版本

此时需要将上述环境部署多份。需要做负载均衡,简单架构图模型。

1

package com.chinda.redis.controller;

import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.IdUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author Wang Chinda
 * @date 2021/4/13
 */
@RestController
@RequestMapping("/product")
public class ProductController {

    public static final String REDIS_LOCK = "lock";

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


    @GetMapping
    public String getTestStr() {
        String value = IdUtil.fastUUID() + Thread.currentThread().getName();

        Boolean absent = redisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value);

        if (!absent) {
            System.out.println("抢锁失败");
            return "抢锁失败";
        }

        Integer result = Convert.toInt(redisTemplate.opsForValue().get("product:001"));

        if (result == null) {
            System.out.println("商品已经售完/活动结束/调用超时, 欢迎下次光临 " + "\t 服务提供端口: " + serverPort);
            return "商品已经售完/活动结束/调用超时, 欢迎下次光临 " + "\t 服务提供端口: " + serverPort;
        }

        int realNumber = result - 1;
        redisTemplate.opsForValue().set("product:001", Convert.toStr(realNumber));
        System.out.println("成功买到商品, 库存还剩下: " + realNumber + "件 \t 服务提供端口" + serverPort);
        redisTemplate.delete(REDIS_LOCK);
        return "成功买到商品, 库存还剩下: " + realNumber + "件 \t 服务提供端口" + serverPort;
    }
}

此时当业务异常,或者程序执行其他分支,会导致Redis锁没有被删除,后续线程就会永远的阻塞了。

考虑lock/unlock配对情况4.0版本

package com.chinda.redis.controller;

import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.IdUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* @author Wang Chinda
* @date 2021/4/13
*/
@RestController
@RequestMapping("/product")
public class ProductController {

   public static final String REDIS_LOCK = "lock";

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


   @GetMapping
   public String getTestStr() {
       String value = IdUtil.fastUUID() + Thread.currentThread().getName();

       Boolean absent = redisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value);

       if (!absent) {
           System.out.println("抢锁失败");
           return "抢锁失败";
       }

       try {
           Integer result = Convert.toInt(redisTemplate.opsForValue().get("product:001"));

           if (result == null) {
               System.out.println("商品已经售完/活动结束/调用超时, 欢迎下次光临 " + "\t 服务提供端口: " + serverPort);
               return "商品已经售完/活动结束/调用超时, 欢迎下次光临 " + "\t 服务提供端口: " + serverPort;
           }

           int realNumber = result - 1;
           redisTemplate.opsForValue().set("product:001", Convert.toStr(realNumber));
           System.out.println("成功买到商品, 库存还剩下: " + realNumber + "件 \t 服务提供端口" + serverPort);
           return "成功买到商品, 库存还剩下: " + realNumber + "件 \t 服务提供端口" + serverPort;
       } finally {
           redisTemplate.delete(REDIS_LOCK);
       }
   }
}

若在加锁以后,解锁之前程序突然宕机,这时锁永远不会被解除。

考虑锁超时5.0版本

package com.chinda.redis.controller;

import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.IdUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.TimeUnit;

/**
 * @author Wang Chinda
 * @date 2021/4/13
 */
@RestController
@RequestMapping("/product")
public class ProductController {

    public static final String REDIS_LOCK = "lock";

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


    @GetMapping
    public String getTestStr() {
        String value = IdUtil.fastUUID() + Thread.currentThread().getName();

        Boolean absent = redisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value);
        redisTemplate.expire(REDIS_LOCK, 10L, TimeUnit.SECONDS);

        if (!absent) {
            System.out.println("抢锁失败");
            return "抢锁失败";
        }

        try {
            Integer result = Convert.toInt(redisTemplate.opsForValue().get("product:001"));

            if (result == null) {
                System.out.println("商品已经售完/活动结束/调用超时, 欢迎下次光临 " + "\t 服务提供端口: " + serverPort);
                return "商品已经售完/活动结束/调用超时, 欢迎下次光临 " + "\t 服务提供端口: " + serverPort;
            }

            int realNumber = result - 1;
            redisTemplate.opsForValue().set("product:001", Convert.toStr(realNumber));
            System.out.println("成功买到商品, 库存还剩下: " + realNumber + "件 \t 服务提供端口" + serverPort);
            return "成功买到商品, 库存还剩下: " + realNumber + "件 \t 服务提供端口" + serverPort;
        } finally {
            redisTemplate.delete(REDIS_LOCK);
        }
    }
}

此时加锁与设置超时时间非原子操作,所以会引起严重的并发问题。

考虑加锁与设置超时原子操作6.0版本

package com.chinda.redis.controller;

import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.IdUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.TimeUnit;

/**
 * @author Wang Chinda
 * @date 2021/4/13
 */
@RestController
@RequestMapping("/product")
public class ProductController {

    public static final String REDIS_LOCK = "lock";

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


    @GetMapping
    public String getTestStr() {
        String value = IdUtil.fastUUID() + Thread.currentThread().getName();

        Boolean absent = redisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value, 10L, TimeUnit.SECONDS);

        if (!absent) {
            System.out.println("抢锁失败");
            return "抢锁失败";
        }

        try {
            Integer result = Convert.toInt(redisTemplate.opsForValue().get("product:001"));

            if (result == null) {
                System.out.println("商品已经售完/活动结束/调用超时, 欢迎下次光临 " + "\t 服务提供端口: " + serverPort);
                return "商品已经售完/活动结束/调用超时, 欢迎下次光临 " + "\t 服务提供端口: " + serverPort;
            }

            int realNumber = result - 1;
            redisTemplate.opsForValue().set("product:001", Convert.toStr(realNumber));
            System.out.println("成功买到商品, 库存还剩下: " + realNumber + "件 \t 服务提供端口" + serverPort);
            return "成功买到商品, 库存还剩下: " + realNumber + "件 \t 服务提供端口" + serverPort;
        } finally {
            redisTemplate.delete(REDIS_LOCK);
        }
    }
}

若业务代码执行超过设置的超时时间, 当业务代码执行完删除锁时会删除其他线程的锁。

考虑删错锁7.0版本

package com.chinda.redis.controller;

import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.IdUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.TimeUnit;

/**
 * @author Wang Chinda
 * @date 2021/4/13
 */
@RestController
@RequestMapping("/product")
public class ProductController {

    public static final String REDIS_LOCK = "lock";

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


    @GetMapping
    public String getTestStr() {
        String value = IdUtil.fastUUID() + Thread.currentThread().getName();

        Boolean absent = redisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value, 10L, TimeUnit.SECONDS);

        if (!absent) {
            System.out.println("抢锁失败");
            return "抢锁失败";
        }

        try {
            Integer result = Convert.toInt(redisTemplate.opsForValue().get("product:001"));

            if (result == null) {
                System.out.println("商品已经售完/活动结束/调用超时, 欢迎下次光临 " + "\t 服务提供端口: " + serverPort);
                return "商品已经售完/活动结束/调用超时, 欢迎下次光临 " + "\t 服务提供端口: " + serverPort;
            }

            int realNumber = result - 1;
            redisTemplate.opsForValue().set("product:001", Convert.toStr(realNumber));
            System.out.println("成功买到商品, 库存还剩下: " + realNumber + "件 \t 服务提供端口" + serverPort);
            return "成功买到商品, 库存还剩下: " + realNumber + "件 \t 服务提供端口" + serverPort;
        } finally {
            if (redisTemplate.opsForValue().get(REDIS_LOCK).equalsIgnoreCase(value)) {
                redisTemplate.delete(REDIS_LOCK);
            }
        }
    }
}

此时判断和删除锁非原子操作。

考虑删除与判断为原子操作8.0版本(Redis事务方式)

package com.chinda.redis.controller;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.IdUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * @author Wang Chinda
 * @date 2021/4/13
 */
@RestController
@RequestMapping("/product")
public class ProductController {

    public static final String REDIS_LOCK = "lock";

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


    @GetMapping
    public String getTestStr() {
        String value = IdUtil.fastUUID() + Thread.currentThread().getName();

        Boolean absent = redisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value, 10L, TimeUnit.SECONDS);

        if (!absent) {
            System.out.println("抢锁失败");
            return "抢锁失败";
        }

        try {
            Integer result = Convert.toInt(redisTemplate.opsForValue().get("product:001"));

            if (result == null) {
                System.out.println("商品已经售完/活动结束/调用超时, 欢迎下次光临 " + "\t 服务提供端口: " + serverPort);
                return "商品已经售完/活动结束/调用超时, 欢迎下次光临 " + "\t 服务提供端口: " + serverPort;
            }

            int realNumber = result - 1;
            redisTemplate.opsForValue().set("product:001", Convert.toStr(realNumber));
            System.out.println("成功买到商品, 库存还剩下: " + realNumber + "件 \t 服务提供端口" + serverPort);
            return "成功买到商品, 库存还剩下: " + realNumber + "件 \t 服务提供端口" + serverPort;
        } finally {
            while (true) {
                redisTemplate.watch(REDIS_LOCK);
                if (redisTemplate.opsForValue().get(REDIS_LOCK).equalsIgnoreCase(value)) {
                    // 开启redis事务
                    redisTemplate.setEnableTransactionSupport(true);
                    redisTemplate.multi();
                    redisTemplate.delete(REDIS_LOCK);
                    List<Object> list = redisTemplate.exec();
                    if (CollUtil.isEmpty(list)) {
                        continue;
                    }
                }
                redisTemplate.unwatch();
                break;
            }
        }
    }
}

考虑删除与判断为原子操作8.1版本(LUA脚本)--推荐

封装简单的Jedis工具

package com.chinda.redis.util;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

/**
 * @author Wang Chinda
 * @date 2021/4/14
 */
public class RedisUtils {
    
    private static JedisPool jedisPool;
    
    static {
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxTotal(20);
        jedisPoolConfig.setMinIdle(10);
        jedisPool = new JedisPool(jedisPoolConfig, "192.168.1.5", 6379);
    }
    
    public static Jedis getJedis() throws Exception {
        if (null != jedisPool) {
            return jedisPool.getResource();
        }
        throw new Exception("Jedis Pool is not ok");
    }
}

修改业务代码

package com.chinda.redis.controller;

import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.IdUtil;
import com.chinda.redis.util.RedisUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import redis.clients.jedis.Jedis;

import java.util.Collections;
import java.util.concurrent.TimeUnit;

/**
 * @author Wang Chinda
 * @date 2021/4/13
 */
@RestController
@RequestMapping("/product")
public class ProductController {

    public static final String REDIS_LOCK = "lock";

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


    @GetMapping
    public String getTestStr() {
        String value = IdUtil.fastUUID() + Thread.currentThread().getName();

        Boolean absent = redisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value, 10L, TimeUnit.SECONDS);

        if (!absent) {
            System.out.println("抢锁失败");
            return "抢锁失败";
        }

        try {
            Integer result = Convert.toInt(redisTemplate.opsForValue().get("product:001"));

            if (result == null) {
                System.out.println("商品已经售完/活动结束/调用超时, 欢迎下次光临 " + "\t 服务提供端口: " + serverPort);
                return "商品已经售完/活动结束/调用超时, 欢迎下次光临 " + "\t 服务提供端口: " + serverPort;
            }

            int realNumber = result - 1;
            redisTemplate.opsForValue().set("product:001", Convert.toStr(realNumber));
            System.out.println("成功买到商品, 库存还剩下: " + realNumber + "件 \t 服务提供端口" + serverPort);
            return "成功买到商品, 库存还剩下: " + realNumber + "件 \t 服务提供端口" + serverPort;
        } finally {
            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 obj = jedis.eval(script, Collections.singletonList(REDIS_LOCK), Collections.singletonList(value));
                if ("1".equals(obj.toString())) {
                    System.out.println("-----delete redis lock ok");
                } else {
                    System.out.println("-----delete redis lock fail");
                }
            } finally {
                if (jedis != null) {
                    jedis.close();
                }
            }
        }
    }
}

此时可满足大部分场景,但是锁超时自动续期问题没有解决。Redis集群模式下,当锁注册到master节点,但是master节点还没来及的同步到slave节点时,master节点宕机,导致锁丢失问题。

Redis集群与锁超时自动续期9.0版本

添加Redisson配置

package com.chinda.redis.config;

import org.redisson.Redisson;
import org.redisson.config.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;

/**
 * @author Wang Chinda
 * @date 2021/4/13
 */
@Configuration
public class RedisConfig {

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

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

编写业务代码

package com.chinda.redis.controller;

import cn.hutool.core.convert.Convert;
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.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author Wang Chinda
 * @date 2021/4/13
 */
@RestController
@RequestMapping("/product")
public class ProductController {

    public static final String REDIS_LOCK = "lock";

    @Autowired
    private StringRedisTemplate redisTemplate;
    @Autowired
    private Redisson redisson;
    @Value("${server.port}")
    private String serverPort;


    @GetMapping
    public String getTestStr() {

        RLock lock = redisson.getLock(REDIS_LOCK);
        lock.lock();
        try {
            Integer result = Convert.toInt(redisTemplate.opsForValue().get("product:001"));

            if (result == null) {
                System.out.println("商品已经售完/活动结束/调用超时, 欢迎下次光临 " + "\t 服务提供端口: " + serverPort);
                return "商品已经售完/活动结束/调用超时, 欢迎下次光临 " + "\t 服务提供端口: " + serverPort;
            }

            int realNumber = result - 1;
            redisTemplate.opsForValue().set("product:001", Convert.toStr(realNumber));
            System.out.println("成功买到商品, 库存还剩下: " + realNumber + "件 \t 服务提供端口" + serverPort);
            return "成功买到商品, 库存还剩下: " + realNumber + "件 \t 服务提供端口" + serverPort;
        } finally {
            lock.unlock();
        }
    }
}

在超高并发下会出现异常, 创建锁的线程与解锁的当前线程不是同一个的问题。

error

考虑超高并发9.1版本

package com.chinda.redis.controller;

import cn.hutool.core.convert.Convert;
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.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author Wang Chinda
 * @date 2021/4/13
 */
@RestController
@RequestMapping("/product")
public class ProductController {

    public static final String REDIS_LOCK = "lock";

    @Autowired
    private StringRedisTemplate redisTemplate;
    @Autowired
    private Redisson redisson;
    @Value("${server.port}")
    private String serverPort;


    @GetMapping
    public String getTestStr() {

        RLock lock = redisson.getLock(REDIS_LOCK);
        lock.lock();
        try {
            Integer result = Convert.toInt(redisTemplate.opsForValue().get("product:001"));

            if (result == null) {
                System.out.println("商品已经售完/活动结束/调用超时, 欢迎下次光临 " + "\t 服务提供端口: " + serverPort);
                return "商品已经售完/活动结束/调用超时, 欢迎下次光临 " + "\t 服务提供端口: " + serverPort;
            }

            int realNumber = result - 1;
            redisTemplate.opsForValue().set("product:001", Convert.toStr(realNumber));
            System.out.println("成功买到商品, 库存还剩下: " + realNumber + "件 \t 服务提供端口" + serverPort);
            return "成功买到商品, 库存还剩下: " + realNumber + "件 \t 服务提供端口" + serverPort;
        } finally {
            if (lock.isLocked() && lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}
posted @ 2021-04-14 11:35  Chinda  阅读(275)  评论(0编辑  收藏  举报