Redis学习——高级篇⑧

Redis学习——高级篇⑧

    • = = = = = = = = = = = = Redis7之实现分布式锁(九) = = = = = = = = = = = =
    • 9.1 分布式锁需要的条件和刚需
    • 9.2 编码
      • 1 搭建环境
      • 2 分布式锁
        • v2.0 - v6.0
        • v7.0 - v8.0
    • 9.3 缺点
  • = = = = = = = = = = = = = = =下面是 转载另一位大佬的分布式锁的介绍 = = = = = = = = = = = = = = =
    • 一、分布式锁
      • 1. 分布式锁的基本原理
      • 2. 分布式锁的特点
      • 3. 分布式锁的实现方式
    • 二、基于Redis的分布式锁
      • 1. 分布式锁执行流程
      • 2\. 基于Redis实现分布式锁的初级版本
    • 三、Redis分布式锁误删问题
      • 1. 误删问题分析
      • 2. 解决方案
      • 3. 改进分布式锁的实现
    • 四、分布式锁的原子性问题
      • 1. 原子性问题分析
      • 2. Lua脚本解决多条命令原子性问题
      • 3. 再次改进Redis的分布式锁
    • 五、小结

在这里插入图片描述

在这里插入图片描述
Redis学习——高级篇⑧_第1张图片

分布式多个不同JVM虚拟机,单机的线程锁机制不再起作用,资源类在不同的服务器之间共享了。

分布式锁,即分布式系统中的锁。在单体应用中我们通过锁解决的是控制共享资源访问的问题,而分布式锁,就是解决了分布式系统中控制共享资源访问的问题。与单体应用不同的是,分布式系统中竞争共享资源的最小粒度从线程升级成了进程。

= = = = = = = = = = = = Redis7之实现分布式锁(九) = = = = = = = = = = = =

9.1 分布式锁需要的条件和刚需

  • 独占性
    • 任何时刻有且只有一个线程持有这个锁
  • 高可用
    • 若redis集群环境下,不能因为某一个节点挂了而出现获取锁和释放锁失败的情况
    • 高并发请求下,依旧性能很好
  • 防死锁
    • 不能出现死锁问题,必须有超时重试机制或者撤销操作,有个终止跳出的途径
  • 不乱抢
    • 防止张冠李戴,只能解锁自己的锁,不能把别人的锁给释放了
  • 重入性
    • 同一节点的同一线程如果获得锁之后,他可以再次获取这个锁

复习一下set
Redis学习——高级篇⑧_第2张图片
Redis学习——高级篇⑧_第3张图片
不可以用setnx作为大厂的分布式锁!!!!

9.2 编码

1 搭建环境

创建工程 redis_distributed_lock2

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.7.10version>
		<relativePath/> 
	parent>
	<groupId>com.xfcygroupId>
	<artifactId>redis_distributed_lock2artifactId>
	<version>0.0.1-SNAPSHOTversion>
	<name>redis_distributed_lock2name>
	<description>redis_distributed_lock2description>
	<properties>
		<java.version>1.8java.version>
		<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
		<maven.compiler.source>8maven.compiler.source>
		<maven.compiler.target>8maven.compiler.target>
		<lombok.version>1.16.18lombok.version>
	properties>
	<dependencies>

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

		<dependency>
			<groupId>org.springframework.bootgroupId>
			<artifactId>spring-boot-starter-webartifactId>
		dependency>

		<dependency>
			<groupId>org.springframework.bootgroupId>
			<artifactId>spring-boot-starter-testartifactId>
			<scope>testscope>
		dependency>

		
		<dependency>
			<groupId>org.springframework.bootgroupId>
			<artifactId>spring-boot-starter-data-redisartifactId>
		dependency>
		<dependency>
			<groupId>org.apache.commonsgroupId>
			<artifactId>commons-pool2artifactId>
		dependency>
		
		<dependency>
			<groupId>io.springfoxgroupId>
			<artifactId>springfox-swagger2artifactId>
			<version>2.9.2version>
		dependency>
		<dependency>
			<groupId>io.springfoxgroupId>
			<artifactId>springfox-swagger-uiartifactId>
			<version>2.9.2version>
		dependency>
		
		<dependency>
			<groupId>org.projectlombokgroupId>
			<artifactId>lombokartifactId>
			<version>${lombok.version}version>
			<optional>trueoptional>
		dependency>
		<dependency>
			<groupId>cn.hutoolgroupId>
			<artifactId>hutool-allartifactId>
			<version>5.8.8version>
		dependency>

	dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.bootgroupId>
				<artifactId>spring-boot-maven-pluginartifactId>
			plugin>
		plugins>
	build>

project>

YML

server.port=7777

spring.application.name=redis_distributed_lock2
# ========================swagger2=====================
# http://localhost:7777/swagger-ui.html
swagger2.enabled=true
spring.mvc.pathmatch.matching-strategy=ant_path_matcher

# ========================redis单机=====================
spring.redis.database=0
spring.redis.host=192.168.238.111
spring.redis.port=6379
spring.redis.password=123456
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-wait=-1ms
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.min-idle=0

主启动类

@SpringBootApplication
public class RedisDistributedLock2Application {

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

业务类

  • Swagger2Config
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

@Configuration
@EnableSwagger2
public class Swagger2Config {

    @Value("${swagger2.enabled}")
    private Boolean enabled;

    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .enable(enabled)
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.xfcy"))
                .paths(PathSelectors.any())
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("springboot利用swagger2构建api接口文档 "+"\t"+ DateTimeFormatter.ofPattern("yyyy-MM-dd").format(LocalDateTime.now()))
                .description("springboot+redis整合")
                .version("1.0")
                .termsOfServiceUrl("https://www.baidu.com/")
                .build();
    }
}
  • RedisConfig
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;

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory)
    {
        RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(lettuceConnectionFactory);
        //设置key序列化方式string
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        //设置value的序列化方式json
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());

        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());

        redisTemplate.afterPropertiesSet();

        return redisTemplate;
    }
}
  • InventoryService
import cn.hutool.core.util.IdUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

@Service
@Slf4j
public class InventoryService
{
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Value("${server.port}")
    private String port;

    private Lock lock = new ReentrantLock();

    public String sale()
    {
        String retMessage = "";
        lock.lock();
        try
        {
            //1 查询库存信息
            String result = stringRedisTemplate.opsForValue().get("inventory001");
            //2 判断库存是否足够
            Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
            //3 扣减库存
            if(inventoryNumber > 0) {
                stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
                retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber;
                System.out.println(retMessage);
            }else{
                retMessage = "商品卖完了,o(╥﹏╥)o";
            }
        }finally {
            lock.unlock();
        }
        return retMessage+"\t"+"服务端口号:"+port;
    }
}
  • InvetoryController
import com.xfcy.service.InventoryService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Api(tags = "redis分布式锁测试")
public class InvetoryController {

    @Autowired
    private InventoryService inventoryService;

    @ApiOperation("扣减库存sale,一次卖一个")
    @GetMapping(value = "/inventory/sale")
    public String sale()
    {
        return inventoryService.sale();
    }


    @ApiOperation("扣减库存saleByRedisson,一次卖一个")
    @GetMapping(value = "/inventory/saleByRedisson")
    public String saleByRedisson()
    {
        return inventoryService.saleByRedisson();
    }
}

2 分布式锁

Redis学习——高级篇⑧_第4张图片

v2.0 - v6.0

InventoryService 实现的一个简单版本的 分布式锁

import cn.hutool.core.util.IdUtil;
import com.xfcy.mylock.DistributedLockFactory;
import lombok.extern.slf4j.Slf4j;
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.stereotype.Service;

import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

@Service
@Slf4j
public class InventoryService {

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

    /**
     *  v6.0       使用 Lua 脚本   将 final的判断 + del 弄成原子操作
     *                  问题: 兼顾锁的可重入性
     *             但是基本 v6.0 版本已经够用
     * @return
     */
    public String sale() {
        String retMessage = "";
        String key = "xfcyRedisLock";
        String value = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();
        while (!stringRedisTemplate.opsForValue().setIfAbsent(key, value, 30L, TimeUnit.SECONDS)) {
            // 暂停20毫秒,进行递归重试
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 抢锁成功的请求线程,进行正常的业务逻辑操作,扣减库存
        try {
            //1 查询库存信息
            String result = stringRedisTemplate.opsForValue().get("inventory001");
            //2 判断库存是否足够
            Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
            //3 扣减库存
            if (inventoryNumber > 0) {
                stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
                retMessage = "成功卖出一个商品,库存剩余: " + inventoryNumber;
                System.out.println(retMessage);
            } else {
                retMessage = "商品卖完了,o(╥﹏╥)o";
            }
        }finally {
            // 改进点,修改为Lua脚本的Redis分布式锁调用,必须保证原子性
            String luaScript =
                    "if (redis.call('get',KEYS[1]) == ARGV[1]) then " +
                            "return redis.call('del',KEYS[1]) " +
                            "else " +
                            "return 0 " +
                            "end";
            stringRedisTemplate.execute(new DefaultRedisScript<>(luaScript, Boolean.class), Arrays.asList(key), value);
        }
        return retMessage + "\t" + "服务端口号:" + port;
    }



    /**
     * v5.0        存在问题: final的判断 + del 不是一行原子操作,需要lua脚本进行修改
     * @return
     */
//    public String sale() {
//        String retMessage = "";
//        String key = "xfcyRedisLock";
//        String value = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();
//        while (!stringRedisTemplate.opsForValue().setIfAbsent(key, value, 30L, TimeUnit.SECONDS)) {
//            // 暂停20毫秒,进行递归重试
//            try {
//                Thread.sleep(20);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
//        }
//        // 抢锁成功的请求线程,进行正常的业务逻辑操作,扣减库存
//        try {
//            //1 查询库存信息
//            String result = stringRedisTemplate.opsForValue().get("inventory001");
//            //2 判断库存是否足够
//            Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//            //3 扣减库存
//            if (inventoryNumber > 0) {
//                stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
//                retMessage = "成功卖出一个商品,库存剩余: " + inventoryNumber;
//                System.out.println(retMessage);
//            } else {
//                retMessage = "商品卖完了,o(╥﹏╥)o";
//            }
//        }finally {
//            // 改进点,只能删除自己的key,而不是别的客户端
//            // 问题:不能保证原子操作
//            if (stringRedisTemplate.opsForValue().get(key).equalsIgnoreCase(value)) {
//                stringRedisTemplate.delete(key);
//            }
//        }
//        return retMessage + "\t" + "服务端口号:" + port;
//    }


    /**
     *  v4.0      加了过期时间 stringRedisTemplate.opsForValue().setIfAbsent(key, value, 30L, TimeUnit.SECONDS)
     *              问题:误删锁,如果线程A运行时间超出了过期时间
     *                      在线程A运行时,xfcyRedisLock这个key过期,另一个线程B进来加了key
     *                      线程A结束后,把线程B的锁删了
     *           stringRedisTemplate.delete(key);  只能自己删除自己的,需要添加判断是否是自己的锁
     * @return
     */
//    public String sale() {
//        String retMessage = "";
//        String key = "xfcyRedisLock";
//        String value = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();
//
//        // 不用递归了,高并发下容易出错,我们用自旋替代递归方法调用;也不用if,用while来替代
//        while (!stringRedisTemplate.opsForValue().setIfAbsent(key, value, 30L, TimeUnit.SECONDS)) {
//            // 暂停20毫秒,进行递归重试
//            try {
//                Thread.sleep(20);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
//        }
//        // 无法保证原子性
        stringRedisTemplate.expire(key, 30L, TimeUnit.SECONDS);
//
//        // 抢锁成功的请求线程,进行正常的业务逻辑操作,扣减库存
//        try {
//            //1 查询库存信息
//            String result = stringRedisTemplate.opsForValue().get("inventory001");
//            //2 判断库存是否足够
//            Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//            //3 扣减库存
//            if (inventoryNumber > 0) {
//                stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
//                retMessage = "成功卖出一个商品,库存剩余: " + inventoryNumber;
//                System.out.println(retMessage);
//            } else {
//                retMessage = "商品卖完了,o(╥﹏╥)o";
//            }
//        }finally {
//            stringRedisTemplate.delete(key);
//        }
//        return retMessage + "\t" + "服务端口号:" + port;
//    }


    /**
     * 3.2版  setnx  用while判断
     *          问题: setnx过后,正在进行业务逻辑操作时,没有走到finally之前,整个微服务down机了,导致锁一直存在
     *                      不是程序出了问题,如果程序问题,最后还是会执行finally
     *                没办法保证解锁(没有过期时间,该key一直存在),需要加入过期时间限定key
     */
//    public String sale() {
//        String retMessage = "";
//        String key = "xfcyRedisLock";
//        String value = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();
//
//        // 不用递归了,高并发下容易出错,我们用自旋替代递归方法调用;也不用if,用while来替代
//        while (!stringRedisTemplate.opsForValue().setIfAbsent(key, value)) {
//            // 暂停20毫秒,进行递归重试
//            try {
//                Thread.sleep(20);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
//        }
//        // 抢锁成功的请求线程,进行正常的业务逻辑操作,扣减库存
//        try {
//            //1 查询库存信息
//            String result = stringRedisTemplate.opsForValue().get("inventory001");
//            //2 判断库存是否足够
//            Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//            //3 扣减库存
//            if (inventoryNumber > 0) {
//                stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
//                retMessage = "成功卖出一个商品,库存剩余: " + inventoryNumber;
//                System.out.println(retMessage);
//            } else {
//                retMessage = "商品卖完了,o(╥﹏╥)o";
//            }
//        }finally {
//            stringRedisTemplate.delete(key);
//        }
//        return retMessage + "\t" + "服务端口号:" + port;
//    }


    /**
     * 3.1版   setnx 递归调用 容易导致 StackOverflowError
     *          高并发唤醒后推荐用while判断而不是if
     * @return
     */
//    public String sale() {
//        String retMessage = "";
//        String key = "xfcyRedisLock";
//        String value = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();
//
//        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, value);
//        // flag = false 抢不到的线程要继续重试 。。。
//        if (!flag) {
//            // 暂停20毫秒,进行递归重试
//            try {
//                Thread.sleep(20);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
//            sale();
//        } else {
//            try {
//                //1 查询库存信息
//                String result = stringRedisTemplate.opsForValue().get("inventory001");
//                //2 判断库存是否足够
//                Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//                //3 扣减库存
//                if (inventoryNumber > 0) {
//                    stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
//                    retMessage = "成功卖出一个商品,库存剩余: " + inventoryNumber;
//                    System.out.println(retMessage);
//                } else {
//                    retMessage = "商品卖完了,o(╥﹏╥)o";
//                }
//            } finally {
//                stringRedisTemplate.delete(key);
//            }
//        }
//        return retMessage + "\t" + "服务端口号:" + port;
//    }


/**
 * v2.0 单机版加锁配合nginx和jmeter压测后,不满足高并发分布式锁的性能要求,出现超卖
 */
//    private Lock lock = new ReentrantLock();
//
//    public String sale() {
//        String retMessage = "";
//        lock.lock();
//        try {
//            //1 查询库存信息
//            String result = stringRedisTemplate.opsForValue().get("inventory001");
//            //2 判断库存是否足够
//            Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//            //3 扣减库存
//            if (inventoryNumber > 0) {
//                stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
//                retMessage = "成功卖出一个商品,库存剩余: " + inventoryNumber;
//                System.out.println(retMessage);
//            } else {
//                retMessage = "商品卖完了,o(╥﹏╥)o";
//            }
//        } finally {
//            lock.unlock();
//        }
//        return retMessage + "\t" + "服务端口号:" + port;
//    }
    

}
v7.0 - v8.0

v 8.0 其实面对不是特别高的并发场景足够用了,单机redis也够用了

  • 要兼顾锁的重入性
    • setnx不满足了,需要hash结构的hset
  • 上锁和解锁都用 Lua 脚本来实现原子性
  • 引入工厂模式 DistributedLockFactory, 实现 Lock 接口,实现redis的可重入锁
    • lock() 加锁的关键逻辑
      • 加锁 实际上就是在redis中,给Key键设置一个值,为避免死锁,并给定一个过期时间
      • 自旋
      • 续期
    • unlock() 解锁关键逻辑
      • 将 Key 键删除,但是也不能乱删,只能自己删自己的锁
  • 实现自动续期功能的完善,后台自定义扫描程序,如果规定时间内没有完成业务逻辑,会调用加钟自动续期的脚本

InventoryService

    @Autowired
    private DistributedLockFactory distributedLockFactory;


    /**
     * v8.0  实现自动续期功能的完善,后台自定义扫描程序,如果规定时间内没有完成业务逻辑,会调用加钟自动续期的脚本
     *
     * @return
     */
    public String sale() {
        String retMessage = "";

        Lock redisLock = distributedLockFactory.getDistributedLock("redis");
        redisLock.lock();
        try {
            //1 查询库存信息
            String result = stringRedisTemplate.opsForValue().get("inventory001");
            //2 判断库存是否足够
            Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
            //3 扣减库存
            if (inventoryNumber > 0) {
                stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
                retMessage = "成功卖出一个商品,库存剩余: " + inventoryNumber;

                // 演示自动续期的的功能
//                try {
//                    TimeUnit.SECONDS.sleep(120);
//                } catch (InterruptedException e) {
//                    e.printStackTrace();
//                }
            } else {
                retMessage = "商品卖完了,o(╥﹏╥)o";
            }
        } finally {
            redisLock.unlock();
        }
        return retMessage + "\t" + "服务端口号:" + port;
    }


    /**
     * v7.0     兼顾锁的可重入性   setnx不满足了,需要hash结构的hset
     * 上锁和解锁都用 Lua 脚本实现原子性
     * 引入工厂模式 DistributedLockFactory    实现Lock接口 ,实现 redis的可重入锁
     *
     * @return
     */
//    //private Lock redisDistributedLock = new RedisDistributedLock(stringRedisTemplate, "xfcyRedisLock");
//
//    public String sale() {
//        String retMessage = "";
//
//        Lock redisLock = distributedLockFactory.getDistributedLock("redis");
//        redisLock.lock();
//
//        //redisDistributedLock.lock();
//        try {
//            //1 查询库存信息
//            String result = stringRedisTemplate.opsForValue().get("inventory001");
//            //2 判断库存是否足够
//            Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//            //3 扣减库存
//            if (inventoryNumber > 0) {
//                stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
//                retMessage = "成功卖出一个商品,库存剩余: " + inventoryNumber;
//                System.out.println(retMessage);
//
//                // 测试可重入性
//                //testReEntry();
//
//            } else {
//                retMessage = "商品卖完了,o(╥﹏╥)o";
//            }
//        } finally {
//            redisLock.unlock();
//            //redisDistributedLock.unlock();
//        }
//        return retMessage + "\t" + "服务端口号:" + port;
//    }
//
//    private void testReEntry() {
//        Lock redisLock = distributedLockFactory.getDistributedLock("redis");
//        redisLock.lock();
//
//        //redisDistributedLock.lock();
//        try {
//            System.out.println("测试可重入锁");
//        } finally {
//            redisLock.unlock();
//            //redisDistributedLock.unlock();
//        }
//    }

mylock/DistributedLockFactory

package com.xfcy.mylock;

import cn.hutool.core.util.IdUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.concurrent.locks.Lock;


@Component
public class DistributedLockFactory {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    private String lockName;
    private String uuidValue;

    public DistributedLockFactory() {
        this.uuidValue = IdUtil.simpleUUID();
    }

    public Lock getDistributedLock(String lockType) {
        if (lockType == null) {
            return null;
        }
        if (lockType.equalsIgnoreCase("REDIS")) {
            this.lockName = "xfcyRedisLock";
            return new RedisDistributedLock(stringRedisTemplate, lockName, uuidValue);
        }else if (lockType.equalsIgnoreCase("ZOOKEEPER")) {
            this.lockName = "xfcyZookeeperLock";
            // TODO zoookeeper 版本的分布式锁
            return null;
        }else if (lockType.equalsIgnoreCase("MYSQL")){
            this.lockName = "xfcyMysqlLock";
            // TODO MYSQL 版本的分布式锁
            return null;
        }
        return null;
    }

}

mylock/RedisDistributedLock

package com.xfcy.mylock;

import cn.hutool.core.util.IdUtil;
import com.sun.org.apache.xpath.internal.operations.Bool;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;

import java.util.Arrays;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;


// @Component 引入DistributedLockFactory工厂模式,从工厂获得即可
public class RedisDistributedLock implements Lock {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    private String lockName;    // KEYS[1]
    private String uuidValue;   // ARGV[1]
    private long expireTime;    // ARGV[2]

    public RedisDistributedLock(StringRedisTemplate stringRedisTemplate, String lockName, String uuidValue) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.lockName = lockName;
        this.uuidValue = uuidValue + ":" + Thread.currentThread().getId();
        this.expireTime = 30L;
    }

    @Override
    public void lock() {
        tryLock();
    }

    @Override
    public boolean tryLock() {
        try {
            tryLock(-1L, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
           e.printStackTrace();
        }
        return false;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        if (time == -1L) {
            String script = "if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then " +
                    "redis.call('hincrby',KEYS[1],ARGV[1],1)  " +
                    "redis.call('expire',KEYS[1],ARGV[2]) " +
                    "return 1 " +
                    "else " +
                    "return 0 " +
                    "end";
            System.out.println("lockName = " + lockName +"\t" + "uuidValue = " + uuidValue);
            while (!stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime))) {
                // 暂停 60ms
                Thread.sleep(60);
            }
            // 新建一个后台扫描程序,来监视key目前的ttl,是否到我们规定的 1/2 1/3 来实现续期
            resetExpire();
            return true;
        }
        return false;
    }

    @Override
    public void unlock() {
        String script = "if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then " +
                "return nil " +
                "elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then  " +
                "return redis.call('del',KEYS[1]) " +
                "else  " +
                "return 0 " +
                "end";
        // nil = false  1 = true  0 = false
        Long flag = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime));
        if (null == flag) {
            throw new RuntimeException("this lock doesn't exists0");
        }
    }

    private void resetExpire() {
        String script = "if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 1 then " +
                "return redis.call('expire',KEYS[1],ARGV[2]) " +
                "else " +
                "return 0 " +
                "end";
        new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                if (stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime))) {
                    resetExpire();
                }
            }
        }, (this.expireTime * 1000) / 3);
    }

}

9.3 缺点

如果我们的分布式锁的服务器挂了,也就是单点故障,我们添加一个从机,但是复制是异步的。

  • Client A 获取 master 中的锁
  • 在对密钥的写入传输到从机之前,主服务器崩溃了
  • 从机被提升为主机
  • Client B 获取对统一资源 A 已持有锁的锁,违反安全规定

在这里插入图片描述

= = = = = = = = = = = = = = =下面是 转载另一位大佬的分布式锁的介绍 = = = = = = = = = = = = = = =

一、分布式锁

1. 分布式锁的基本原理

分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁。

Redis学习——高级篇⑧_第5张图片
Redis学习——高级篇⑧_第6张图片
Redis学习——高级篇⑧_第7张图片
让多个进程看到同一个锁监视器,而且要实现互斥,也是就只有一个线程能拿到线程锁!

2. 分布式锁的特点

Redis学习——高级篇⑧_第8张图片

3. 分布式锁的实现方式

  • 分布式锁的核心是实现多进程之间互斥,而满足这一点的方式有很多,常见的有三种:
X MySQL Redis Zookeeper
互斥 利用mysq 本身的互斥锁机制 利用setnx这样的互斥命令
高可用
高性能 一般 一般
安全性 断开连接,自动释放锁 利用锁超时时间,到期释放 临时节点,断开连接自动释放

二、基于Redis的分布式锁

1. 分布式锁执行流程

实现分布式锁时需要实现的两个基本方法:

  • 获取锁:

    • 互斥:确保只能有一个线程获取锁
    • 非阻塞:尝试一次,成功返回true,失败返回false
# 添加锁,NX是互斥,EX是设置超时时间
SET lock thread NX EX 10

Redis学习——高级篇⑧_第9张图片

  • 释放锁:

    • 手动释放
    • 超时释放:获取锁时添加一个超时时间
# 释放锁,删除即可
DEL key

Redis学习——高级篇⑧_第10张图片

2. 基于Redis实现分布式锁的初级版本

需求:定义一个类,实现下面接口,利用Redis实现分布式锁功能。

public interface ILock {

    /**
     * 非阻塞方式,尝试获取锁
     * @param timeoutSec 锁持有的超时时间,过期后自动释放
     * @return true代表获取锁成功; false代表获取锁失败
     */
    boolean tryLock(long timeoutSec);

    /**
     * 释放锁
     */
    void unlock();
}

简单实现redis分布式锁:

public class SimpleRedisLock implements ILock {

	// 业务名称
    private String name;
    private StringRedisTemplate stringRedisTemplate;

	// 通过构造方法将name和stringRedisTemplate传入
    public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
        this.name = name;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    private static final String KEY_PREFIX = "lock:";

    @Override
    public boolean tryLock(long timeoutSec) {
        // 获取线程标识
        long threadId = Thread.currentThread().getId();
        // 获取锁
        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unlock() {
        //通过del删除锁
        stringRedisTemplate.delete(KEY_PREFIX + name);
    }
}

改造上一章中加synchronized锁的代码:

  • 使用synchronized锁
synchronized (userId.toString().intern()) {
    // 获取代理对象(事务)
    IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
   return proxy.createVoucherOrder(voucherId);
}
  • 使用Redis分布式锁
// 使用Redis分布式锁
// 创建锁对象
SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
// 获取锁对象
boolean isLock = lock.tryLock(5);
// 加锁失败
if (!isLock) {
   return Result.fail("不允许重复下单");
}
try {
   // 获取代理对象(事务)
   IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
   return proxy.createVoucherOrder(voucherId);
} finally {
   // 释放锁
   lock.unlock();
}

三、Redis分布式锁误删问题

1. 误删问题分析

  • 线程1先获取锁后,由于业务阻塞还没执行完成,线程1的锁超时后自动释放
  • 线程2在线程1的锁超时自动释放后,进行加锁成功
  • 正好线程1将业务接着执行完后,需要释放锁,此时释放的就是线程2的锁,造成了误删问题
  • 误删后,线程3又加锁成功,此时,线程2和线程3就出现了并发执行业务,造成并发安全问题

Redis学习——高级篇⑧_第11张图片
Redis学习——高级篇⑧_第12张图片

Redis学习——高级篇⑧_第13张图片

2. 解决方案

  • 在获取锁时存入线程标识(可以用UUID表示)

  • 在释放锁时先获取锁中的线程标识,判断是否与当前线程标识一致

    • 如果一致则释放锁
    • 如果不一致则不释放锁
      注意:不要直接将线程id作为线程标识,因为不同JVM中的线程id可能一样,所以可以用 线程id+UUID 作为线程标识

3. 改进分布式锁的实现

public class SimpleRedisLock implements ILock {

    // 业务名称
    private String name;
    private StringRedisTemplate stringRedisTemplate;

    // 通过构造方法将name和stringRedisTemplate传入
    public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
        this.name = name;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    private static final String KEY_PREFIX = "lock:";
    private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";

    @Override
    public boolean tryLock(long timeoutSec) {
        // 获取线程标识
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        // 获取锁
        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unlock() {
        // 获取线程标识
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        // 获取锁中的标识
        String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
        // 判断标识是否一致
        if(threadId.equals(id)) {
            // 释放锁
            stringRedisTemplate.delete(KEY_PREFIX + name);
        }
    }
}

四、分布式锁的原子性问题

1. 原子性问题分析

  • 线程1执行完业务后,准备释放锁
  • 先判断完锁一致后,正准备释放时,发生了阻塞(例如:GC时所有线程会阻塞),恰好线程1在阻塞期间,锁超时被释放
  • 线程2获取锁成功,此时线程1被唤醒后,继续释放锁,由于之前判断过锁的标识,所以直接释放锁,但是此时的锁是线程2的
  • 线程3又加锁成功,此时,线程2和线程3就出现了并发执行业务,造成并发安全问题

Redis学习——高级篇⑧_第14张图片

2. Lua脚本解决多条命令原子性问题

Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。Lua是一种编程语言,它的基本语法大家可以参考网站:https://www.runoob.com/lua/lua-tutorial.html

这里重点介绍Redis提供的调用函数,语法如下:

# 执行Redis命令
redis.call('命令名称', 'key', '其他参数', ...)

例如,我们要执行set name jack,则脚本是这样:

# 执行 set name jack
redis.call('set', 'name', 'jack')

例如,我们要先执行set name Rose,再执行get name,则脚本如下:

# 先执行 set name jack
redis.call('set', 'name', 'jack')
# 再执行 get name
local name = redis.call('get', 'name')
# 返回
return name

写好脚本以后,需要用Redis命令来调用脚本,调用脚本的常见命令如下:

127.0.0.1:6379> help @scripting
  EVAL script numkeys key [key ...] arg [arg ...]
  summary: Execute a Lua script server side
  since: 2.6.0

例如,我们要执行 redis.call(‘set’, ‘name’, ‘jack’) 这个脚本,语法如下:

在这里插入图片描述

如果脚本中的key、value不想写死,可以作为参数传递。key类型参数会放入KEYS数组,其它参数会放入ARGV数组,在脚本中可以从KEYS和ARGV数组获取这些参数:

在这里插入图片描述

释放锁的业务流程是这样的:

  1. 获取锁中的线程标识
  2. 判断是否与指定的标识(当前线程标识)一致
  3. 如果一致则释放锁(删除)
  4. 如果不一致则什么都不做

如果用Lua脚本来表示则是这样的:

-- 这里的 KEYS[1] 就是锁的key,这里的ARGV[1] 就是当前线程标识
-- 获取锁中的标识,判断是否与当前线程标识一致
if (redis.call('GET', KEYS[1]) == ARGV[1]) then
  -- 一致,则删除锁
  return redis.call('DEL', KEYS[1])
end
-- 不一致,则直接返回
return 0

3. 再次改进Redis的分布式锁

需求:基于Lua脚本实现分布式锁的释放锁逻辑

提示:RedisTemplate调用Lua脚本的API如下:

Redis学习——高级篇⑧_第15张图片

  • 将编写的Lua脚本放在resources目录下:

Redis学习——高级篇⑧_第16张图片

public class SimpleRedisLock implements ILock {

    // 业务名称
    private String name;
    private StringRedisTemplate stringRedisTemplate;

    // 通过构造方法将name和stringRedisTemplate传入
    public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
        this.name = name;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    private static final String KEY_PREFIX = "lock:";
    private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
    // 加载Lua脚本
    private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
    static {
        UNLOCK_SCRIPT = new DefaultRedisScript<>();
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
        UNLOCK_SCRIPT.setResultType(Long.class);
    }

    @Override
    public boolean tryLock(long timeoutSec) {
        // 获取线程标识
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        // 获取锁
        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unlockL() {
        // 调用Lua脚本
        stringRedisTemplate.execute(
                UNLOCK_SCRIPT,
                Collections.singletonList(KEY_PREFIX + name),
                ID_PREFIX + Thread.currentThread().getId());
    }

五、小结

基于Redis的分布式锁实现思路:

  • 利用set nx ex获取锁,并设置过期时间,保存线程标识
  • 释放锁时先判断线程标识是否与自己一致,一致则删除锁

特性:

  • 利用set nx满足互斥性
  • 利用set ex保证故障时锁依然能释放,避免死锁,提高安全性
  • 利用Redis集群保证高可用和高并发特性

你可能感兴趣的:(Redis,redis,学习,数据库)