zoukankan      html  css  js  c++  java
  • 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("商品已经售完/活动结束/调用超时, 欢迎下次光临 " + "	 服务提供端口: " + serverPort);
                return "商品已经售完/活动结束/调用超时, 欢迎下次光临 " + "	 服务提供端口: " + serverPort;
            }
    
            int realNumber = result - 1;
            redisTemplate.opsForValue().set("product:001", Convert.toStr(realNumber));
            System.out.println("成功买到商品, 库存还剩下: " + realNumber + "件 	 服务提供端口" + serverPort);
            return "成功买到商品, 库存还剩下: " + realNumber + "件 	 服务提供端口" + 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("商品已经售完/活动结束/调用超时, 欢迎下次光临 " + "	 服务提供端口: " + serverPort);
                    return "商品已经售完/活动结束/调用超时, 欢迎下次光临 " + "	 服务提供端口: " + serverPort;
                }
    
                int realNumber = result - 1;
                redisTemplate.opsForValue().set("product:001", Convert.toStr(realNumber));
                System.out.println("成功买到商品, 库存还剩下: " + realNumber + "件 	 服务提供端口" + serverPort);
                return "成功买到商品, 库存还剩下: " + realNumber + "件 	 服务提供端口" + 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("商品已经售完/活动结束/调用超时, 欢迎下次光临 " + "	 服务提供端口: " + serverPort);
                return "商品已经售完/活动结束/调用超时, 欢迎下次光临 " + "	 服务提供端口: " + serverPort;
            }
    
            int realNumber = result - 1;
            redisTemplate.opsForValue().set("product:001", Convert.toStr(realNumber));
            System.out.println("成功买到商品, 库存还剩下: " + realNumber + "件 	 服务提供端口" + serverPort);
            redisTemplate.delete(REDIS_LOCK);
            return "成功买到商品, 库存还剩下: " + realNumber + "件 	 服务提供端口" + 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("商品已经售完/活动结束/调用超时, 欢迎下次光临 " + "	 服务提供端口: " + serverPort);
                   return "商品已经售完/活动结束/调用超时, 欢迎下次光临 " + "	 服务提供端口: " + serverPort;
               }
    
               int realNumber = result - 1;
               redisTemplate.opsForValue().set("product:001", Convert.toStr(realNumber));
               System.out.println("成功买到商品, 库存还剩下: " + realNumber + "件 	 服务提供端口" + serverPort);
               return "成功买到商品, 库存还剩下: " + realNumber + "件 	 服务提供端口" + 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("商品已经售完/活动结束/调用超时, 欢迎下次光临 " + "	 服务提供端口: " + serverPort);
                    return "商品已经售完/活动结束/调用超时, 欢迎下次光临 " + "	 服务提供端口: " + serverPort;
                }
    
                int realNumber = result - 1;
                redisTemplate.opsForValue().set("product:001", Convert.toStr(realNumber));
                System.out.println("成功买到商品, 库存还剩下: " + realNumber + "件 	 服务提供端口" + serverPort);
                return "成功买到商品, 库存还剩下: " + realNumber + "件 	 服务提供端口" + 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("商品已经售完/活动结束/调用超时, 欢迎下次光临 " + "	 服务提供端口: " + serverPort);
                    return "商品已经售完/活动结束/调用超时, 欢迎下次光临 " + "	 服务提供端口: " + serverPort;
                }
    
                int realNumber = result - 1;
                redisTemplate.opsForValue().set("product:001", Convert.toStr(realNumber));
                System.out.println("成功买到商品, 库存还剩下: " + realNumber + "件 	 服务提供端口" + serverPort);
                return "成功买到商品, 库存还剩下: " + realNumber + "件 	 服务提供端口" + 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("商品已经售完/活动结束/调用超时, 欢迎下次光临 " + "	 服务提供端口: " + serverPort);
                    return "商品已经售完/活动结束/调用超时, 欢迎下次光临 " + "	 服务提供端口: " + serverPort;
                }
    
                int realNumber = result - 1;
                redisTemplate.opsForValue().set("product:001", Convert.toStr(realNumber));
                System.out.println("成功买到商品, 库存还剩下: " + realNumber + "件 	 服务提供端口" + serverPort);
                return "成功买到商品, 库存还剩下: " + realNumber + "件 	 服务提供端口" + 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("商品已经售完/活动结束/调用超时, 欢迎下次光临 " + "	 服务提供端口: " + serverPort);
                    return "商品已经售完/活动结束/调用超时, 欢迎下次光临 " + "	 服务提供端口: " + serverPort;
                }
    
                int realNumber = result - 1;
                redisTemplate.opsForValue().set("product:001", Convert.toStr(realNumber));
                System.out.println("成功买到商品, 库存还剩下: " + realNumber + "件 	 服务提供端口" + serverPort);
                return "成功买到商品, 库存还剩下: " + realNumber + "件 	 服务提供端口" + 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("商品已经售完/活动结束/调用超时, 欢迎下次光临 " + "	 服务提供端口: " + serverPort);
                    return "商品已经售完/活动结束/调用超时, 欢迎下次光临 " + "	 服务提供端口: " + serverPort;
                }
    
                int realNumber = result - 1;
                redisTemplate.opsForValue().set("product:001", Convert.toStr(realNumber));
                System.out.println("成功买到商品, 库存还剩下: " + realNumber + "件 	 服务提供端口" + serverPort);
                return "成功买到商品, 库存还剩下: " + realNumber + "件 	 服务提供端口" + 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("商品已经售完/活动结束/调用超时, 欢迎下次光临 " + "	 服务提供端口: " + serverPort);
                    return "商品已经售完/活动结束/调用超时, 欢迎下次光临 " + "	 服务提供端口: " + serverPort;
                }
    
                int realNumber = result - 1;
                redisTemplate.opsForValue().set("product:001", Convert.toStr(realNumber));
                System.out.println("成功买到商品, 库存还剩下: " + realNumber + "件 	 服务提供端口" + serverPort);
                return "成功买到商品, 库存还剩下: " + realNumber + "件 	 服务提供端口" + 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("商品已经售完/活动结束/调用超时, 欢迎下次光临 " + "	 服务提供端口: " + serverPort);
                    return "商品已经售完/活动结束/调用超时, 欢迎下次光临 " + "	 服务提供端口: " + serverPort;
                }
    
                int realNumber = result - 1;
                redisTemplate.opsForValue().set("product:001", Convert.toStr(realNumber));
                System.out.println("成功买到商品, 库存还剩下: " + realNumber + "件 	 服务提供端口" + serverPort);
                return "成功买到商品, 库存还剩下: " + realNumber + "件 	 服务提供端口" + serverPort;
            } finally {
                if (lock.isLocked() && lock.isHeldByCurrentThread()) {
                    lock.unlock();
                }
            }
        }
    }
    
  • 相关阅读:
    最小二乘拟合(转)good
    会议论文重新投稿算不算侵权?这肯定是所多人都遇到过的问题(转)
    吝啬的国度
    压力单位MPa、Psi和bar之间换算公式
    Oracle建立表空间和用户
    layoutSubviews总结
    C++中出现的计算机术语4
    445port入侵具体解释
    hdu
    ORM框架
  • 原文地址:https://www.cnblogs.com/chinda/p/14657080.html
Copyright © 2011-2022 走看看