分布式存储极致性能Redis(4)

分布式存储极致性能Redis

一、Redis分布式锁

1.1 锁的分类

  • 单机版同一个JVM虚拟机内synchronized或者Lock接口
  • 分布式不同个JVM虚拟机内,单机的线程锁机制不再起作用,资源类在不同的服务器之间共享了。

1.2 分布式锁需要具备的条件和刚需

  1. 独占性
    OnlyOne,任何时刻只能有且仅有一个线程持有
  2. 高可用
    若redis集群环境下,不能因为某一个节点挂了而出现获取锁和释放锁失败的情况
  3. 防死锁
    杜绝死锁,必须有超时控制机制或者撤销操作,有个兜底终止跳出方案
  4. 不乱抢
    防止张冠李戴,不能私下unlock别人的锁,只能自己加锁自己释放。
  5. 重入性
    同一个节点的同一个线程如果获得锁之后,它也可以再次获取这个锁。

1.3 分布式锁

在这里插入图片描述

  • setnx key value
    在这里插入图片描述
    setnx+expire不安全,两条命令非原子性的

  • set key value [EX seconds] [PX milliseconds] [NX|XX]’

1.4 案例(springboot+redis)

1.4.1 使用场景

多个服务间保证同一时刻同一时间段内同一用户只能有一个请求(防止关键业务出现并发攻击)

1.4.2 建Module(两个module内容相同,除端口号不同)

boot_redis01
boot_redis02

1.4.3 pom.xml

<?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.3.10.RELEASE</version>
        <relativePath/>
    </parent>

    <groupId>com.atguigu.redis</groupId>
    <artifactId>boot_redis01</artifactId>
    <version>0.0.1-SNAPSHOT</version>

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

    <dependencies>
        <!--guava-->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>23.0</version>
        </dependency>
        <!--web+actuator-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--SpringBootRedis整合依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
        <!-- jedis -->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>3.1.0</version>
        </dependency>
        <!-- springboot-aop 技术-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <!-- redisson -->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.13.4</version>
        </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>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

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

</project>
 

1.4.4 写yml

application.yml


# 端口号
server.port=1111

# ========================redis相关配置=====================
# Redis数据库索引(默认为0)
spring.redis.database=0  
# Redis服务器地址
spring.redis.host=192.168.111.140
# 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.min-idle=0

1.4.5 编写主启动类

@SpringBootApplication
public class BootRedis01Application
{
    
    

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

}

1.4.6 config包和controller包

@Configuration
public class RedisConfig {
    
    

    @Bean
    public RedisTemplate<String, Serializable> redisTemplate(LettuceConnectionFactory connectionFactory)
    {
    
    
        RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate<>();

        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        redisTemplate.setConnectionFactory(connectionFactory);

        return redisTemplate;
    }
}
 
@RestController
public class GoodController
{
    
    
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

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

    @GetMapping("/buy_goods")
    public String buy_Goods()
    {
    
    
        String result = stringRedisTemplate.opsForValue().get("goods:001");
        int goodsNumber = result == null ? 0 : Integer.parseInt(result);

        if(goodsNumber > 0)
        {
    
    
            int realNumber = goodsNumber - 1;
            stringRedisTemplate.opsForValue().set("goods:001",realNumber + "");
            System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口:"+serverPort);
            return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口:"+serverPort;
        }else{
    
    
            System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口:"+serverPort);
        }
        
        return "商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口:"+serverPort;
    }
}

1.4.7 测试

在这里插入图片描述
在这里插入图片描述

1.5 分析上面案例的问题并完善代码(下面每一个案例都是对前一个案例的问题修复)

1.5.1 案例1(解决单机版的高并发的超卖问题)

问题

没有加锁,高并发环境下,出现超卖现象

解决方案

可以使用synchronized或者ReetranLock

  • synchronized, lock.lock()(阻塞)
  • lock.tryLock(): 尝试获取锁,获取不到直接返回(非阻塞)
  • lock.tryLock(2L,TimeUnit.SECONDS)): 等待2s没获取到锁就直接返回(过时不候)

代码

@RestController
public class GoodController
{
    
    
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    private final Lock lock = new ReentrantLock();

    @GetMapping("/buy_goods")
    public String buyGoods() throws InterruptedException
    {
    
    
        /*synchronized (this)
        {
            String number = stringRedisTemplate.opsForValue().get("goods:001");

            int realNumber = number == null ? 0 : Integer.parseInt(number);

            if(realNumber > 0)
            {
                realNumber = realNumber - 1;
                stringRedisTemplate.opsForValue().set("goods:001",String.valueOf(realNumber));
                return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件";
            }
        }*/

        //if (lock.tryLock(2L,TimeUnit.SECONDS))
        if (lock.tryLock())
        {
    
    
            try
            {
    
    
                String number = stringRedisTemplate.opsForValue().get("goods:001");

                int realNumber = number == null ? 0 : Integer.parseInt(number);

                if(realNumber > 0)
                {
    
    
                    realNumber = realNumber - 1;
                    stringRedisTemplate.opsForValue().set("goods:001",String.valueOf(realNumber));
                    return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件";
                }
            }finally {
    
    
                lock.unlock();
            }
        }
        return "商品售罄/活动结束,欢迎下次光临";
    }
}
 

总结

在单机环境下,可以使用synchronized或Lock来实现。

但是在分布式系统中,因为竞争的线程可能不在同一个节点上(同一个jvm中),
所以需要一个让所有进程都能访问到的锁来实现(比如redis或者zookeeper来构建)

不同进程jvm层面的锁就不管用了,那么可以利用第三方的一个组件,来获取锁,未获取到锁,则阻塞当前想要运行的线程

1.5.2 案例2(nginx分布式微服务架构)(解决单机锁在分布式环境下没用的问题)

问题

  • 分布式部署后,单机锁还是出现超卖现象,需要分布式锁
    在这里插入图片描述

Nginx配制负载均衡

命令地址+配置地址
  • 命令地址
    /usr/local/nginx/sbin
  • 配置地址
    /usr/local/nginx/conf

配制nginx.conf
在这里插入图片描述

nginx重载配置

/usr/local/nginx/sbin目录下执行如下命令

./nginx -s reload
nginx指定配置文件

/usr/local/nginx/sbin/目录下执行命令

./nginx -c /usr/local/nginx/conf/nginx.conf
基础命令(/usr/local/nginx/sbin)
  • 启动
./nginx
  • 关闭
./nginx -s stop
  • 刷新配置文件
./nginx -s reload

启动两个服务

  • 通过Nginx访问,反向代理+负载均衡
    • http://192.168.111.147/buy_goods
    • 可以点击看到效果,一边一个,默认轮询

jmeter模拟高并发请求

在这里插入图片描述

线程组redis

在这里插入图片描述

http请求

在这里插入图片描述

jmeter压测(在分布式环境下出现了超卖现象,单机锁已经无法满足分布式的环境了)

在这里插入图片描述

解决方案

  • redis分布式锁setnx
    在这里插入图片描述

  • 官网

代码
@RestController
public class GoodController
{
    
    
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Value("${server.port}")
    private String serverPort;

    @GetMapping("/buy_goods")
    public String buy_Goods()
    {
    
    
        String key = "zzyyRedisLock";
        String value = UUID.randomUUID().toString()+Thread.currentThread().getName();

        Boolean flagLock = stringRedisTemplate.opsForValue().setIfAbsent(key, value);
        if(!flagLock)
        {
    
    
            return "抢夺锁失败,o(╥﹏╥)o";
        }

        String result = stringRedisTemplate.opsForValue().get("goods:001");
        int goodsNumber = result == null ? 0 : Integer.parseInt(result);

        if(goodsNumber > 0)
        {
    
    
            int realNumber = goodsNumber - 1;
            stringRedisTemplate.opsForValue().set("goods:001",realNumber + "");
            stringRedisTemplate.delete(key);
            System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口:"+serverPort);
            return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口:"+serverPort;
        }else{
    
    
            System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口:"+serverPort);
        }

        return "商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口:"+serverPort;


    }
}
 

1.5.3 案例3

问题

上面一个案例的代码在出异常时,可能无法释放锁,必须要在代码层面finally释放锁
必须使用finally关闭锁资源

解决

加锁解锁,lock/unlock必须同时出现并保证调用

代码
@RestController
public class GoodController
{
    
    
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Value("${server.port}")
    private String serverPort;

    @GetMapping("/buy_goods")
    public String buy_Goods()
    {
    
    
        String key = "zzyyRedisLock";
        String value = UUID.randomUUID().toString()+Thread.currentThread().getName();

        try {
    
    
            Boolean flagLock = stringRedisTemplate.opsForValue().setIfAbsent(key, value);
            if(!flagLock)
            {
    
    
                return "抢锁失败,o(╥﹏╥)o";
            }

            String result = stringRedisTemplate.opsForValue().get("goods:001");
            int goodsNumber = result == null ? 0 : Integer.parseInt(result);

            if(goodsNumber > 0)
            {
    
    
                int realNumber = goodsNumber - 1;
                stringRedisTemplate.opsForValue().set("goods:001",realNumber + "");
                System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口:"+serverPort);
                return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口:"+serverPort;
            }else{
    
    
                System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口:"+serverPort);
            }
            return "商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口:"+serverPort;
        } finally {
    
    
            stringRedisTemplate.delete(key);
        }
    }
}
 

1.5.4 案例4

问题

部署了微服务jar包的机器挂了,代码层面根本没有走到finally这块,
没办法保证解锁,这个key没有被删除,需要加入一个过期时间限定key

解决

对key加上过期时间

代码
@RestController
public class GoodController
{
    
    
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Value("${server.port}")
    private String serverPort;

    @GetMapping("/buy_goods")
    public String buy_Goods()
    {
    
    
        String key = "zzyyRedisLock";
        String value = UUID.randomUUID().toString()+Thread.currentThread().getName();

        try {
    
    
            Boolean flagLock = stringRedisTemplate.opsForValue().setIfAbsent(key, value);

            stringRedisTemplate.expire(key,10L,TimeUnit.SECONDS);

            if(!flagLock)
            {
    
    
                return "抢锁失败,o(╥﹏╥)o";
            }

            String result = stringRedisTemplate.opsForValue().get("goods:001");
            int goodsNumber = result == null ? 0 : Integer.parseInt(result);

            if(goodsNumber > 0)
            {
    
    
                int realNumber = goodsNumber - 1;
                stringRedisTemplate.opsForValue().set("goods:001",realNumber + "");
                System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口:"+serverPort);
                return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口:"+serverPort;
            }else{
    
    
                System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口:"+serverPort);
            }
            return "商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口:"+serverPort;
        } finally {
    
    
            stringRedisTemplate.delete(key);
        }
    }
}
 

1.5.5 案例5

问题

设置key+过期时间分开了,必须要合并成一行具备原子性

解决

将设置key和过期时间放到一个命令,保证原子性

代码
@RestController
public class GoodController
{
    
    
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Value("${server.port}")
    private String serverPort;

    @GetMapping("/buy_goods")
    public String buy_Goods()
    {
    
    
        String key = "zzyyRedisLock";
        String value = UUID.randomUUID().toString()+Thread.currentThread().getName();

        try {
    
    
            Boolean flagLock = stringRedisTemplate.opsForValue().setIfAbsent(key,value,10L,TimeUnit.SECONDS);

            if(!flagLock)
            {
    
    
                return "抢锁失败,o(╥﹏╥)o";
            }

            String result = stringRedisTemplate.opsForValue().get("goods:001");
            int goodsNumber = result == null ? 0 : Integer.parseInt(result);

            if(goodsNumber > 0)
            {
    
    
                int realNumber = goodsNumber - 1;
                stringRedisTemplate.opsForValue().set("goods:001",realNumber + "");
                System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口:"+serverPort);
                return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口:"+serverPort;
            }else{
    
    
                System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口:"+serverPort);
            }
            return "商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口:"+serverPort;
        } finally {
    
    
            stringRedisTemplate.delete(key);
        }
    }
}

1.5.6 案例6

问题

张冠李戴,可能在设置的过期时间业务还未完成,锁已经被删了,然后finally块中就可能会删除别的服务创建的锁。

解决

只能删除自己创建的锁,不能动别人的

代码
@RestController
public class GoodController
{
    
    
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Value("${server.port}")
    private String serverPort;

    @GetMapping("/buy_goods")
    public String buy_Goods()
    {
    
    
        String key = "zzyyRedisLock";
        String value = UUID.randomUUID().toString()+Thread.currentThread().getName();

        try {
    
    
            Boolean flagLock = stringRedisTemplate.opsForValue().setIfAbsent(key,value,10L,TimeUnit.SECONDS);

            if(!flagLock)
            {
    
    
                return "抢锁失败,o(╥﹏╥)o";
            }

            String result = stringRedisTemplate.opsForValue().get("goods:001");
            int goodsNumber = result == null ? 0 : Integer.parseInt(result);

            if(goodsNumber > 0)
            {
    
    
                int realNumber = goodsNumber - 1;
                stringRedisTemplate.opsForValue().set("goods:001",realNumber + "");
                System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口:"+serverPort);
                return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口:"+serverPort;
            }else{
    
    
                System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口:"+serverPort);
            }
            return "商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口:"+serverPort;
        } finally {
    
    
            if (stringRedisTemplate.opsForValue().get(key).equals(value)) {
    
    
                stringRedisTemplate.delete(key);
            }
        }
    }
}
 

1.5.7 案例7

问题

finally块的判断+del删除操作不是原子性的

解决

使用Lua脚本
Redis调用Lua脚本通过eval命令保证代码执行的原子性

代码

RedisUtils

public class RedisUtils
{
    
    
    private static JedisPool jedisPool;

    static {
    
    
        JedisPoolConfig jedisPoolConfig=new JedisPoolConfig();
        jedisPoolConfig.setMaxTotal(20);
        jedisPoolConfig.setMaxIdle(10);
        jedisPool=new JedisPool(jedisPoolConfig,"192.168.111.147",6379);
    }

    public static Jedis getJedis() throws Exception {
    
    
        if(null!=jedisPool){
    
    
            return jedisPool.getResource();
        }
        throw new Exception("Jedispool was not init");
    }

}
 
@RestController
public class GoodController
{
    
    
    public static final String REDIS_LOCK_KEY = "redisLockPay";

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

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


    @GetMapping("/buy_goods")
    public String buy_Goods()
    {
    
    
        String value = UUID.randomUUID().toString()+Thread.currentThread().getName();

        try {
    
    
            Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK_KEY, value,30L,TimeUnit.SECONDS);

            if(!flag)
            {
    
    
                return "抢夺锁失败,请下次尝试";
            }

            String result = stringRedisTemplate.opsForValue().get("goods:001");
            int goodsNumber = result == null ? 0 : Integer.parseInt(result);

            if(goodsNumber > 0)
            {
    
    
                int realNumber = goodsNumber - 1;
                stringRedisTemplate.opsForValue().set("goods:001",realNumber + "");
                System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口:"+serverPort);
                return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口:"+serverPort;
            }else{
    
    
                System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口:"+serverPort);
            }
            return "商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\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 result = jedis.eval(script, Collections.singletonList(REDIS_LOCK_KEY), Collections.singletonList(value));
                if ("1".equals(result.toString())) {
    
    
                    System.out.println("------del REDIS_LOCK_KEY success");
                }else{
    
    
                    System.out.println("------del REDIS_LOCK_KEY error");
                }
            } finally {
    
    
                if(null != jedis) {
    
    
                    jedis.close();
                }
            }

        }
    }
}

小结

截止到这里,基于单个Redis节点实现分布式锁

1.5.8 集群环境

问题

  • 确保redisLock过期时间大于业务执行时间的问题
    • Redis分布式锁如何续期?

集群+CAP(redis对比zookeeper)

  • Redis单机是CP集群是AP
    在这里插入图片描述

  • Redis集群(AP)
    redis异步复制造成的锁丢失,
    比如:主节点没来的及把刚刚set进来这条数据给从节点,master就挂了,从机上位但从机上无该数据

  • Zookeeper集群的CAP
    在这里插入图片描述
    故障
    在这里插入图片描述

1.5.9 案例 8

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

基于redis集群实现高可用的分布式锁

使用redisson

代码
@Configuration
public class RedisConfig {
    
    
    @Bean
    public Redisson redisson()
    {
    
    
        Config config = new Config();

        config.useSingleServer().setAddress("redis://192.168.111.140:6379").setDatabase(0);

        return (Redisson) Redisson.create(config);
    }
}
@RestController
public class GoodController
{
    
    
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Value("${server.port}")
    private String serverPort;
    @Autowired
    private Redisson redisson;

    @GetMapping("/buy_goods")
    public String buy_Goods(){
    
    
        String key = "zzyyRedisLock";
        RLock redissonLock = redisson.getLock(key);
        redissonLock.lock();
        try{
    
    
            String result = stringRedisTemplate.opsForValue().get("goods:001");
            int goodsNumber = result == null ? 0 : Integer.parseInt(result);

            if(goodsNumber > 0)
            {
    
    
                int realNumber = goodsNumber - 1;
                stringRedisTemplate.opsForValue().set("goods:001",realNumber + "");
                System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口:"+serverPort);
                return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口:"+serverPort;
            }else{
    
    
                System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口:"+serverPort);
            }
            return "商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口:"+serverPort;
        }finally {
    
    
            redissonLock.unlock();
        }
    }
}
 
相关网址

https://redis.io/topics/distlock
https://github.com/redisson/redisson
https://redisson.org/
https://github.com/redisson/redisson/wiki/8.-distributed-locks-and-synchronizers#84-redlock

上面的代码bug及完善

可能接了别服务创建的锁,所以解锁时需要判断当前锁是否是自己创建的那个,避免张冠李戴。
在这里插入图片描述

@RestController
public class GoodController
{
    
    
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Value("${server.port}")
    private String serverPort;
    @Autowired
    private Redisson redisson;

    @GetMapping("/buy_goods")
    public String buy_Goods()
    {
    
    
        String key = "zzyyRedisLock";

        RLock redissonLock = redisson.getLock(key);
        redissonLock.lock();

        try
        {
    
    
            String result = stringRedisTemplate.opsForValue().get("goods:001");
            int goodsNumber = result == null ? 0 : Integer.parseInt(result);

            if(goodsNumber > 0)
            {
    
    
                int realNumber = goodsNumber - 1;
                stringRedisTemplate.opsForValue().set("goods:001",realNumber + "");
                System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口:"+serverPort);
                return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口:"+serverPort;
            }else{
    
    
                System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口:"+serverPort);
            }
            return "商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口:"+serverPort;
        }finally {
    
    
            if(redissonLock.isLocked() && redissonLock.isHeldByCurrentThread())
            {
    
    
              redissonLock.unlock();
            }
        }
    }
}
 

二、Redis分布式锁-Redlock算法(Distributed locks with Redis)

官网说明

在这里插入图片描述

2.1 使用场景

多个服务间保证同一时刻同一时间段内同一用户只能有一个请求(防止关键业务出现并发攻击)

  • Redis分布式锁比较正确的姿势是采用redisson这个客户端工具

2.2 RedLock落地实现(Redisson)

2.2.1 RedLock理念

2.2.2 redisson实现

2.3 单机案例

三个重要元素

  • 加锁
    加锁实际上就是在redis中,给Key键设置一个值,为避免死锁,并给定一个过期时间
  • 解锁
    • 将Key键删除。但也不能乱删,不能说客户端1的请求将客户端2的锁给删除掉,只能自己删除自己的锁
    • lua
      为了保证解锁操作的原子性,我们用LUA脚本完成这一操作。先判断当前锁的字符串是否与传入的值相等,是的话就删除Key,解锁成功。
if redis.call('get',KEYS[1]) == ARGV[1] then 
   return redis.call('del',KEYS[1]) 
else
   return 0 
end
  • 超时
    锁key要注意过期时间,不能长期占用

面试回答的主要考点

加锁关键逻辑

在这里插入图片描述

public static boolean tryLock(String key, String uniqueId, int seconds) {
    
    
    return "OK".equals(jedis.set(key, uniqueId, "NX", "EX", seconds));
}
解锁关键逻辑

在这里插入图片描述

public static boolean releaseLock(String key, String uniqueId) {
    
    
    String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
            "return redis.call('del', KEYS[1]) else return 0 end";
    
    return jedis.eval(
            luaScript,
            Collections.singletonList(key),
            Collections.singletonList(uniqueId)
    ).equals(1L);
}

单机模式中,一般都是用set/setnx+lua脚本搞定,想想它的缺点是什么?

  • 上面一般中小公司,不是高并发场景,是可以使用的。单机redis小业务也撑得住

2.4 多机案例

2.4.1 基于setnx的分布式锁有什么缺点?

在这里插入图片描述

线程 1 首先获取锁成功,将键值对写入 redis 的 master 节点;
在 redis 将该键值对同步到 slave 节点之前,master 发生了故障;
redis 触发故障转移,其中一个 slave 升级为新的 master;
此时新的 master 并不包含线程 1 写入的键值对,因此线程 2 尝试获取锁也可以成功拿到锁;
此时相当于有两个线程获取到了锁,可能会导致各种预期之外的情况发生,例如最常见的脏数据。

我们加的是排它独占锁,同一时间只能有一个建redis锁成功并持有锁,严禁出现2个以上的请求线程拿到锁。危险的

2.4.2 redis之父提出了RedLock算法解决了这个问题

Redis也提供了Redlock算法,用来实现基于多个实例的分布式锁。
锁变量由多个实例维护,即使有实例发生了故障,锁变量仍然是存在的,客户端还是可以完成锁操作。Redlock算法是实现高可靠分布式锁的一种有效解决方案,可以在实际开发中使用。

在这里插入图片描述

RedLock设计理念

该方案也是基于(set 加锁、Lua 脚本解锁)进行改良的,所以redis之父antirez 只描述了差异的地方,大致方案如下。
假设我们有N个Redis主节点,例如 N = 5这些节点是完全独立的,我们不使用复制或任何其他隐式协调系统,
为了取到锁客户端执行以下操作:

在这里插入图片描述

该方案为了解决数据不一致的问题,直接舍弃了异步复制只使用 master 节点,同时由于舍弃了 slave,为了保证可用性,引入了 N 个节点,官方建议是 5。演示用3台实例来做说明。
客户端只有在满足下面的这两个条件时,才能认为是加锁成功。
条件1:客户端从超过半数(大于等于N/2+1)的Redis实例上成功获取到了锁;
条件2:客户端获取锁的总耗时没有超过锁的有效时间。

解决方案

在这里插入图片描述

为什么是奇数? N = 2X + 1 (N是最终部署机器数,X是容错机器数)
1 先知道什么是容错
失败了多少个机器实例后我还是可以容忍的,所谓的容忍就是数据一致性还是可以Ok的,CP数据一致性还是可以满足
加入在集群环境中,redis失败1台,可接受。2X+1 = 2 * 1+1 =3,部署3台,死了1个剩下2个可以正常工作,那就部署3台。
加入在集群环境中,redis失败2台,可接受。2X+1 = 2 * 2+1 =5,部署5台,死了2个剩下3个可以正常工作,那就部署5台。

2 为什么是奇数?
最少的机器,最多的产出效果
加入在集群环境中,redis失败1台,可接受。2N+2= 2 * 1+2 =4,部署4台
加入在集群环境中,redis失败2台,可接受。2N+2 = 2 * 2+2 =6,部署6台

案例

  • docker走起3台redis的master机器,本次设置3台master各自独立无从属关系
    docker run -p 6381:6379 --name redis-master-1 -d redis:6.0.7

docker run -p 6382:6379 --name redis-master-2 -d redis:6.0.7

docker run -p 6383:6379 --name redis-master-3 -d redis:6.0.7

执行成功见下:
在这里插入图片描述

-进入上一步刚启动的redis容器实例
在这里插入图片描述
docker exec -it redis-master-1 /bin/bash

docker exec -it redis-master-2 /bin/bash

docker exec -it redis-master-3 /bin/bash

建Module

redis_redlock

pom.xml

 
<?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.3.10.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>com.atguigu.redis.redlock</groupId>
    <artifactId>redis_redlock</artifactId>
    <version>0.0.1-SNAPSHOT</version>


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

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <!--<version>3.12.0</version>-->
            <version>3.13.4</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.8</version>
        </dependency>
        <!--swagger-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
        <!--swagger-ui-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.4</version>
            <scope>compile</scope>
        </dependency>

    </dependencies>

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

</project>

yml

 
spring.application.name=spring-boot-redis
server.port=9090

spring.swagger2.enabled=true


spring.redis.database=0
spring.redis.password=
spring.redis.timeout=3000
#sentinel/cluster/single
spring.redis.mode=single

spring.redis.pool.conn-timeout=3000
spring.redis.pool.so-timeout=3000
spring.redis.pool.size=10

spring.redis.single.address1=192.168.111.147:6381
spring.redis.single.address2=192.168.111.147:6382
spring.redis.single.address3=192.168.111.147:6383

主启动

业务类

CacheConfiguration

@Configuration
@EnableConfigurationProperties(RedisProperties.class)
public class CacheConfiguration {
    
    

    @Autowired
    RedisProperties redisProperties;

    @Bean
    RedissonClient redissonClient1() {
    
    
        Config config = new Config();
        String node = redisProperties.getSingle().getAddress1();
        node = node.startsWith("redis://") ? node : "redis://" + node;
        SingleServerConfig serverConfig = config.useSingleServer()
                .setAddress(node)
                .setTimeout(redisProperties.getPool().getConnTimeout())
                .setConnectionPoolSize(redisProperties.getPool().getSize())
                .setConnectionMinimumIdleSize(redisProperties.getPool().getMinIdle());
        if (StringUtils.isNotBlank(redisProperties.getPassword())) {
    
    
            serverConfig.setPassword(redisProperties.getPassword());
        }
        return Redisson.create(config);
    }

    @Bean
    RedissonClient redissonClient2() {
    
    
        Config config = new Config();
        String node = redisProperties.getSingle().getAddress2();
        node = node.startsWith("redis://") ? node : "redis://" + node;
        SingleServerConfig serverConfig = config.useSingleServer()
                .setAddress(node)
                .setTimeout(redisProperties.getPool().getConnTimeout())
                .setConnectionPoolSize(redisProperties.getPool().getSize())
                .setConnectionMinimumIdleSize(redisProperties.getPool().getMinIdle());
        if (StringUtils.isNotBlank(redisProperties.getPassword())) {
    
    
            serverConfig.setPassword(redisProperties.getPassword());
        }
        return Redisson.create(config);
    }

    @Bean
    RedissonClient redissonClient3() {
    
    
        Config config = new Config();
        String node = redisProperties.getSingle().getAddress3();
        node = node.startsWith("redis://") ? node : "redis://" + node;
        SingleServerConfig serverConfig = config.useSingleServer()
                .setAddress(node)
                .setTimeout(redisProperties.getPool().getConnTimeout())
                .setConnectionPoolSize(redisProperties.getPool().getSize())
                .setConnectionMinimumIdleSize(redisProperties.getPool().getMinIdle());
        if (StringUtils.isNotBlank(redisProperties.getPassword())) {
    
    
            serverConfig.setPassword(redisProperties.getPassword());
        }
        return Redisson.create(config);
    }
}
 

RedisPoolProperties

 
package com.atguigu.redis.redlock.config;

import lombok.Data;

@Data
public class RedisPoolProperties {
    
    

    private int maxIdle;

    private int minIdle;

    private int maxActive;

    private int maxWait;

    private int connTimeout;

    private int soTimeout;

    /**
     * 池大小
     */
    private  int size;

}
 

RedisProperties

@ConfigurationProperties(prefix = "spring.redis", ignoreUnknownFields = false)
@Data
public class RedisProperties {
    
    

    private int database;

    /**
     * 等待节点回复命令的时间。该时间从命令发送成功时开始计时
     */
    private int timeout;

    private String password;

    private String mode;

    /**
     * 池配置
     */
    private RedisPoolProperties pool;

    /**
     * 单机信息配置
     */
    private RedisSingleProperties single;


}

RedisSingleProperties

@Data
public class RedisSingleProperties {
    
    
    private  String address1;
    private  String address2;
    private  String address3;
}

RedLockCController

@RestController
@Slf4j
public class RedLockController {
    
    

    public static final String CACHE_KEY_REDLOCK = "ZZYY_REDLOCK";

    @Autowired
    RedissonClient redissonClient1;

    @Autowired
    RedissonClient redissonClient2;

    @Autowired
    RedissonClient redissonClient3;

    @GetMapping(value = "/redlock")
    public void getlock() {
    
    
        //CACHE_KEY_REDLOCK为redis 分布式锁的key
        RLock lock1 = redissonClient1.getLock(CACHE_KEY_REDLOCK);
        RLock lock2 = redissonClient2.getLock(CACHE_KEY_REDLOCK);
        RLock lock3 = redissonClient3.getLock(CACHE_KEY_REDLOCK);

        RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
        boolean isLock;

        try {
    
    

            //waitTime 锁的等待时间处理,正常情况下 等5s
            //leaseTime就是redis key的过期时间,正常情况下等5分钟。
            isLock = redLock.tryLock(5, 300, TimeUnit.SECONDS);
            log.info("线程{},是否拿到锁:{} ",Thread.currentThread().getName(),isLock);
            if (isLock) {
    
    
                //TODO if get lock success, do something;
                //暂停20秒钟线程
                try {
    
     TimeUnit.SECONDS.sleep(20); } catch (InterruptedException e) {
    
     e.printStackTrace(); }
            }
        } catch (Exception e) {
    
    
            log.error("redlock exception ",e);
        } finally {
    
    
            // 无论如何, 最后都要解锁
            redLock.unlock();
            System.out.println(Thread.currentThread().getName()+"\t"+"redLock.unlock()");
        }
    }


}
 
 

测试

http://localhost:9090/redlock
在这里插入图片描述

2.5 Redisson源码分析

public class WatchDogDemo
{
    
    
    public static final String LOCKKEY = "AAA";

    private static Config config;
    private static Redisson redisson;

    static {
    
    
        config = new Config();
        config.useSingleServer().setAddress("redis://"+"192.168.111.147"+":6379").setDatabase(0);
        redisson = (Redisson)Redisson.create(config);
    }

    public static void main(String[] args)
    {
    
    
        RLock redissonLock = redisson.getLock(LOCKKEY);

        redissonLock.lock();
        try
        {
    
    
            System.out.println("1111");
            //暂停几秒钟线程
            try {
    
     TimeUnit.SECONDS.sleep(25); } catch (InterruptedException e) {
    
     e.printStackTrace(); }
        }catch (Exception e){
    
    
            e.printStackTrace();
        }finally {
    
    
           redissonLock.unlock();
        }

        System.out.println(Thread.currentThread().getName() + " main ------ ends.");

        //暂停几秒钟线程
        try {
    
     TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) {
    
     e.printStackTrace(); }
        redisson.shutdown();
    }
}
 
 

守护线程“缓存续命”

额外起一个线程,定期检查线程是否还持有锁,如果有则延长过期时间。

Redisson 里面就实现了这个方案,使用“看门狗”定期检查(每1/3的锁时间检查1次),如果线程还持有锁,则刷新过期时间;

  • 在获取锁成功后,给锁加一个 watchdog,watchdog 会起一个定时任务,在锁没有被释放且快要过期的时候会续期

在这里插入图片描述

在这里插入图片描述

源码分析1

  • 通过redisson新建出来的锁key,默认是30秒

在这里插入图片描述
在这里插入图片描述

源码分析2

在这里插入图片描述

源码分析3(watch dog自动延期机制)

在这里插入图片描述

这里面初始化了一个定时器,dely 的时间是 internalLockLeaseTime/3。
在 Redisson 中,internalLockLeaseTime 是 30s,也就是每隔 10s 续期一次,每次 30s。

在这里插入图片描述

源码分析4(watch dog自动延期机制)

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

流程解释

  • 通过exists判断,如果锁不存在,则设置值和过期时间,加锁成功
  • 通过hexists判断,如果锁已存在,并且锁的是当前线程,则证明是重入锁,加锁成功
  • 如果锁已存在,但锁的不是当前线程,则证明有其他线程持有锁。
    返回当前锁的过期时间(代表了lockzzyy这个锁key的剩余生存时间),加锁失败

加锁查看

public class WatchDogDemo
{
    
    
    public static final String LOCKKEY = "AAA";
    private static Config config;
    private static Redisson redisson;

    static {
    
    
        config = new Config();
        config.useSingleServer().setAddress("redis://"+"192.168.111.147"+":6379").setDatabase(0);
        redisson = (Redisson)Redisson.create(config);
    }

    public static void main(String[] args)
    {
    
    
        RLock redissonLock = redisson.getLock(LOCKKEY);

        redissonLock.lock();
        try
        {
    
    
            System.out.println("1111");
            //暂停几秒钟线程
            try {
    
     TimeUnit.SECONDS.sleep(25); } catch (InterruptedException e) {
    
     e.printStackTrace(); }
        }catch (Exception e){
    
    
            e.printStackTrace();
        }finally {
    
    
            if (redissonLock.isLocked() && redissonLock.isHeldByCurrentThread()) {
    
    
                redissonLock.unlock();
            }
        }

        System.out.println(Thread.currentThread().getName() + " main ------ ends.");

        //暂停几秒钟线程
        try {
    
     TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) {
    
     e.printStackTrace(); }
        redisson.shutdown();
    }
}

加锁成功后,在redis的内存数据中,就有一条hash结构的数据。
Key为锁的名称;field为随机字符串+线程ID;值为1。见下

在这里插入图片描述

如果同一线程多次调用lock方法,值递增1。

可重入锁查看

在这里插入图片描述

ttl续命的演示

加大业务逻辑处理时间,看超过10秒钟后,redisson的续命加时

解锁

在这里插入图片描述

常见异常情况

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

正确写法

@RestController
public class GoodController
{
    
    
    public static final String REDIS_LOCK_KEY = "lockzzyy";

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

    @GetMapping("/buy_goods")
    public String buy_Goods() throws IOException
    {
    
    
        RLock redissonLock = redisson.getLock(REDIS_LOCK_KEY);
        redissonLock.lock();
        try
        {
    
    
            String result = stringRedisTemplate.opsForValue().get("goods:001");
            int goodsNumber = result == null ? 0 : Integer.parseInt(result);

            if(goodsNumber > 0)
            {
    
    
                int realNumber = goodsNumber - 1;
                stringRedisTemplate.opsForValue().set("goods:001",realNumber + "");
                System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口:"+serverPort);
                //暂停几秒钟线程
                try {
    
     TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) {
    
     e.printStackTrace(); }
                return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口:"+serverPort;
            }else{
    
    
                System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口:"+serverPort);
            }
            return "商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口:"+serverPort;
        }finally {
    
    
            if(redissonLock.isLocked() && redissonLock.isHeldByCurrentThread())
            {
    
    
                redissonLock.unlock();
            }
        }
    }
}

 
 

猜你喜欢

转载自blog.csdn.net/qq_43478625/article/details/121306135
今日推荐