Redis7实战加面试题-高阶篇(手写Redis分布式锁)

news/2024/10/19 5:27:39/

手写Redis分布式锁

面试题:
1.Redis除了拿来做缓存,你还见过基于Redis的什么用法?
数据共享,分布式session分布式锁
全局ID
计算器、点赞位统计
购物车
轻量级消息队列(list,stream)
抽奖
点赞、签到、打卡
差集交集并集,用户关注、可能认识的人,推荐模型
热点新闻、热搜排行榜
2.Redis做分布式锁的时候有需要注意的问题?
3.你们公司自己实现的分布式锁是否用的setnx命令实现?这个是最合适的吗?你如何考虑分布式锁的可重入问题?
4.如果是Redis是单点部署的,会带来什么问题?
5.Redis集群模式下,比如主从模式,CAP方面有没有什么问题呢?
6.那你简单的介绍一下Redlock 吧?你简历上写redisson,你谈谈
7.Redis分布式锁如何续期?看门狗知道吗?

锁的种类

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

一个靠谱分布式锁需要具备的条件和刚需

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

分布式锁

在这里插入图片描述
setnx key value:
在这里插入图片描述
set key value [ EX seconds ] [ PX milliseconds ] [NX|XX]

重点:

JUC中AQs锁的规范落地参考+可重入锁考虑+Lua脚本+Redis命令一步步实现分布式锁

Base案例(boot+redis):

使用场景:多个服务间保证同一时刻同一时间段内同一用户只能有一个请求(防止关键业务出现并发攻击)
建Module:redis_distributed_lock2,redis_distributed_lock3
改POM:

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.atguigu.redislock</groupId><artifactId>redis_distributed_lock2</artifactId><version>1.0-SNAPSHOT</version><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.6.12</version><relativePath/> <!-- lookup parent from repository --></parent><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target><lombok.version>1.16.18</lombok.version></properties><dependencies><!--SpringBoot通用依赖模块--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!--SpringBoot与Redis整合依赖--><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><!--swagger2--><dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger2</artifactId><version>2.9.2</version></dependency><dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger-ui</artifactId><version>2.9.2</version></dependency><!--通用基础配置boottest/lombok/hutool--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>${lombok.version}</version><optional>true</optional></dependency><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.8</version></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>

写YML:

server.port=7777spring.application.name=redis_distributed_lock
# ========================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.111.185
spring.redis.port=6379
spring.redis.password=111111
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

主启动:

package com.atguigu.redislock;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;/*** @auther zzyy* @create 2022-10-12 22:20*/
@SpringBootApplication
public class RedisDistributedLockApp7777
{public static void main(String[] args){SpringApplication.run(RedisDistributedLockApp7777.class,args);}
}

业务类:
Swagger2Config

package com.atguigu.redislock.config;import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping;
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.spring.web.plugins.WebMvcRequestHandlerProvider;
import springfox.documentation.swagger2.annotations.EnableSwagger2;import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.stream.Collectors;/*** @auther zzyy* @create 2022-10-12 21:55*/
@Configuration
@EnableSwagger2
public class Swagger2Config
{@Value("${swagger2.enabled}")private Boolean enabled;@Beanpublic Docket createRestApi() {return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).enable(enabled).select().apis(RequestHandlerSelectors.basePackage("com.atguigu.redislock")) //你自己的package.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:

package com.atguigu.redislock.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;/*** @auther zzyy* @create 2022-07-02 11:25*/
@Configuration
public class RedisConfig
{@Beanpublic RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory){RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();redisTemplate.setConnectionFactory(lettuceConnectionFactory);//设置key序列化方式stringredisTemplate.setKeySerializer(new StringRedisSerializer());//设置value的序列化方式jsonredisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());redisTemplate.setHashKeySerializer(new StringRedisSerializer());redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());redisTemplate.afterPropertiesSet();return redisTemplate;}
}

lnventoryService

package com.atguigu.redislock.service;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;/*** @auther zzyy* @create 2022-10-22 15:14*/
@Service
@Slf4j
public class InventoryService
{@Autowiredprivate 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;}
}

lnventoryController:

package com.atguigu.redislock.controller;import cn.hutool.core.util.IdUtil;
import com.atguigu.redislock.service.InventoryService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.Getter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;import java.util.concurrent.atomic.AtomicInteger;/*** @auther zzyy* @create 2022-10-12 17:05*/
@RestController
@Api(tags = "redis分布式锁测试")
public class InventoryController
{@Autowiredprivate InventoryService inventoryService;@ApiOperation("扣减库存,一次卖一个")@GetMapping(value = "/inventory/sale")public String sale(){return inventoryService.sale();}
}

手写分布式锁思路分析2023

1.初始化版本简单添加
业务类:lnventoryService

package com.atguigu.redislock.service;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;/*** @auther zzyy* @create 2022-10-22 15:14*/
@Service
@Slf4j
public class InventoryService
{@Autowiredprivate 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;}
}

请将7777的业务逻辑代码原样拷贝到8888
加了synchronized或者Lock
2.nginx分布式微服务架构
问题:
1.V2.0版本代码分布式部署后,单机锁还是出现超卖现象,需要分布式锁
在这里插入图片描述
2.Nginx配置负载均衡
命令地址:/usr/local/nginx/sbin
配置地址:/usr/local/nginx/conf
启动:/usr/local/nginx/sbin -> ./nginx,启动Nginx并测试通过,浏览器看到nginx欢迎welcome页面
/usr/local/nginx/conf目录下修改配置文件,nginx.conf新增反向代理和负载均衡配置
在这里插入图片描述
关闭:/usr/local/nginx/sbin -> ./nginx -s stop
指定配置启动:
在这里插入图片描述
在/usr/local/nginx/sbin路径下执行下面的命令:./nginx -c /usr/local/nginx/conf/nginx.conf
重启:/usr/local/nginx/sbin -> ./nginx -s reload
3.V2.0版本代码修改+启动两个微服务
7777:InventoryService

package com.atguigu.redislock.service;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;/*** @auther zzyy* @create 2022-10-22 15:14*/
@Service
@Slf4j
public class InventoryService
{@Autowiredprivate 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;}
}

8888:InventoryService

package com.atguigu.redislock.service;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;/*** @auther zzyy* @create 2022-10-22 15:14*/
@Service
@Slf4j
public class InventoryService
{@Autowiredprivate 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;}
}

通过Nginx访问,你的Linux服务器地址IP,反向代理+负载均衡
可以点击看到效果,一边一个,默认轮询

4.上面纯手点验证OK,下面高并发模拟
线程组redis:100个商品足够了
在这里插入图片描述
http请求:
在这里插入图片描述

jmeter压测:
在这里插入图片描述
76号商品被卖出2次,出现超卖故障现象
5.bug-why:
为什么加了synchronized或者Lock还是没有控制住?
在单机环境下,可以使用synchronized或Lock来实现。
但是在分布式系统中,因为竞争的线程可能不在同一个节点上(同一个jvm中),
所以需要一个让所有进程都能访问到的锁来实现(比如redis或者zookeeper来构建)
不同进程jvm层面的锁就不管用了,那么可以利用第三方的一个组件,来获取锁,未获取到锁,则阻塞当前想要运行的线程
6.分布式锁出现:
能干嘛:1.跨进程+跨服务2.解决超卖3.防止缓存击穿
解决:
上redis分布式锁setnx
在这里插入图片描述
官网:https://redis.io/commands/set

3.redis分布式锁

修改为3.1版

package com.atguigu.redislock.service;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.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;/*** @auther zzyy* @create 2022-10-22 15:14*/
@Service
@Slf4j
public class InventoryService
{@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Value("${server.port}")private String port;private Lock lock = new ReentrantLock();public String sale(){String retMessage = "";String key = "zzyyRedisLock";String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue);if(!flag){//暂停20毫秒后递归调用try { TimeUnit.MILLISECONDS.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;}
}

通过递归重试的方式。测试手工OK,测试Jmeter压测5000,OK。递归是一种思想没错,但是容易导致stackOverflowError,不太推荐,进一步完善
修改为3.2版:

package com.atguigu.redislock.service;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.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;/*** @auther zzyy* @create 2022-10-22 15:14*/
@Service
@Slf4j
public class InventoryService
{@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Value("${server.port}")private String port;private Lock lock = new ReentrantLock();public String sale(){String retMessage = "";String key = "zzyyRedisLock";String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();while(!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue)){//暂停20毫秒,类似CAS自旋try { TimeUnit.MILLISECONDS.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;}
}

多线程判断想想JUC里面说过的虚假唤醒,用while替代if,用自旋替代递归重试

4.宕机与过期+防止死锁
当前代码为3.2版接上一步
问题:部署了微服务的Java程序机器挂了,代码层面根本没有走到finally这块,没办法保证解锁(无过期时间该key一直存在),这个key没有被删除,需要加入一个过期时间限定key
解决:
修改为4.1版

while(!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue))
{//暂停20毫秒,进行递归重试.....try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
}stringRedisTemplate.expire(key,30L,TimeUnit.SECONDS);

4.1版本结论:设置key+过期时间分开了,必须要合并成—行具备原子性
在这里插入图片描述
修改为4.2 版

package com.atguigu.redislock.service;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.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;/*** @auther zzyy* @create 2022-10-22 15:14*/
@Service
@Slf4j
public class InventoryService
{@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Value("${server.port}")private String port;private Lock lock = new ReentrantLock();public String sale(){String retMessage = "";String key = "zzyyRedisLock";String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();while(!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue,30L,TimeUnit.SECONDS)){//暂停毫秒try { TimeUnit.MILLISECONDS.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;}
}

Jmeter压测oK
4.2版本结论:加锁和过期时间设置必须同一行,保证原子性

5.防止误删key的问题
当前代码为4.2版接上一步
问题:
实际业务处理时间如果超过了默认设置key的过期时间??
张冠李戴,删除了别人的锁
在这里插入图片描述
解决:只能自己删除自己的,不许动别人的
修改为5.0版

package com.atguigu.redislock.service;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.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;/*** @auther zzyy* @create 2022-10-22 15:14*/
@Service
@Slf4j
public class InventoryService
{@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Value("${server.port}")private String port;private Lock lock = new ReentrantLock();public String sale(){String retMessage = "";String key = "zzyyRedisLock";String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();while(!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue,30L,TimeUnit.SECONDS)){//暂停毫秒try { TimeUnit.MILLISECONDS.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+"\t"+uuidValue;System.out.println(retMessage);}else{retMessage = "商品卖完了,o(╥﹏╥)o";}}finally {// v5.0判断加锁与解锁是不是同一个客户端,同一个才行,自己只能删除自己的锁,不误删他人的if(stringRedisTemplate.opsForValue().get(key).equalsIgnoreCase(uuidValue)){stringRedisTemplate.delete(key);}}return retMessage+"\t"+"服务端口号:"+port;}
}

6.Lua保证原子性
当前代码为5.0版接上一步

package com.atguigu.redislock.service;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.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;/*** @auther zzyy* @create 2022-10-22 15:14*/
@Service
@Slf4j
public class InventoryService
{@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Value("${server.port}")private String port;private Lock lock = new ReentrantLock();public String sale(){String retMessage = "";String key = "zzyyRedisLock";String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();while(!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue,30L,TimeUnit.SECONDS)){//暂停毫秒try { TimeUnit.MILLISECONDS.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+"\t"+uuidValue;System.out.println(retMessage);}else{retMessage = "商品卖完了,o(╥﹏╥)o";}}finally {// v5.0判断加锁与解锁是不是同一个客户端,同一个才行,自己只能删除自己的锁,不误删他人的if(stringRedisTemplate.opsForValue().get(key).equalsIgnoreCase(uuidValue)){stringRedisTemplate.delete(key);}}return retMessage+"\t"+"服务端口号:"+port;}
}

问题:finally块的判断+del删除操作不是原子性的
启用lua脚本编写redis分布式锁判断+删除判断代码
在这里插入图片描述
官网:https://redis.io/docs/reference/patterns/distributed-locks/
官方脚本:https://redis.io/docs/reference/patterns/distributed-locks/
在这里插入图片描述

Lua脚本初识:Redis调用Lua脚本通过eval命令保证代码执行的原子性,直接用return返回脚本执行后的结果值
eval luascript numkeys [key [key …]] [arg [arg …]]
helloworld入门:
在这里插入图片描述
2-set k1 v1 get k1
在这里插入图片描述
3-mset
在这里插入图片描述
Lua脚本进—步:
Redis分布式锁lua脚本官网练习
在这里插入图片描述条件判断语法
在这里插入图片描述
条件判断案例
在这里插入图片描述
解决:
修改为6.0版code

package com.atguigu.redislock.service;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.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;/*** @auther zzyy* @create 2022-10-22 15:14*/
@Service
@Slf4j
public class InventoryService
{@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Value("${server.port}")private String port;private Lock lock = new ReentrantLock();public String sale(){String retMessage = "";String key = "zzyyRedisLock";String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();while(!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue,30L,TimeUnit.SECONDS)){//暂停毫秒try { TimeUnit.MILLISECONDS.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+"\t"+uuidValue;System.out.println(retMessage);}else{retMessage = "商品卖完了,o(╥﹏╥)o";}}finally {//V6.0 将判断+删除自己的合并为lua脚本保证原子性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), uuidValue);}return retMessage+"\t"+"服务端口号:"+port;}
}

bug说明
在这里插入图片描述
7.可重入锁+设计模式
当前代码为6.0版接上一步
while判断并自旋重试获取锁+setnx含自然过期时间+Lua脚本官网删除锁命令。
问题:如何兼顾锁的可重入性问题?
在这里插入图片描述
可重入锁(又名递归锁):是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。如果是1个有 synchronized 修饰的递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚。所以Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
“可重入锁"这四个字分开来解释:可:可以。重:再次。入:进入。锁:同步锁。进入什么(进入同步域(即同步代码块/方法或显式锁锁定的代码))。
—句话:一个线程中的多个流程可以获取同一把锁,持有这把同步锁可以再次进入。自己可以获取自己的内部锁
可重入锁种类:
1.隐式锁(即synchronized关键字使用的锁)默认是可重入锁
指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。简单的来说就是:在一个synchronized修饰的方法或代码块的内部调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁的。与可重入锁相反,不可重入锁不可递归调用,递归调用就发生死锁。
2.Synchronized的重入的实现机理
每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么 Java 虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。
3.显式锁(即Lock)也有ReentrantLock这样的可重入锁。

package com.atguigu.juc.senior.prepare;import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;/*** @auther zzyy* @create 2020-05-14 11:59* 在一个Synchronized修饰的方法或代码块的内部调用本类的其他Synchronized修饰的方法或代码块时,是永远可以得到锁的*/
public class ReEntryLockDemo
{static Lock lock = new ReentrantLock();public static void main(String[] args){new Thread(() -> {lock.lock();try{System.out.println("----外层调用lock");lock.lock();try{System.out.println("----内层调用lock");}finally {// 这里故意注释,实现加锁次数和释放次数不一样// 由于加锁次数和释放次数不一样,第二个线程始终无法获取到锁,导致一直在等待。lock.unlock(); // 正常情况,加锁几次就要解锁几次}}finally {lock.unlock();}},"a").start();new Thread(() -> {lock.lock();try{System.out.println("b thread----外层调用lock");}finally {lock.unlock();}},"b").start();}
}

思考+设计重点(一横—纵):
目前有2条支线,目的是保证同一个时候只能有一个线程持有锁进去redis做扣减库存动作。
2个分支:
1.保证加锁/解锁,lock/unlock

2.扣减库存redis命令的原子性
在这里插入图片描述
lua脚本:
redis命令过程分析:
在这里插入图片描述
加锁lua脚本lock
先判断redis分布式锁这个key是否存在。EXISTS key。返回零说明不存在,hset新建当前线程属于自己的锁BY UUID:ThreadID
在这里插入图片描述
返回壹说明已经有锁,需进一步判断是不是当前线程自己的。HEXISTS key uuid:ThreadlD
在这里插入图片描述
返回零说明不是自己的,返回壹说明是自己的锁,自增1次表示重入
在这里插入图片描述

上述设计修改为Lua脚本:
V1

if redis.call('exists','key') == 0 thenredis.call('hset','key','uuid:threadid',1)redis.call('expire','key',30)return 1elseif redis.call('hexists','key','uuid:threadid') == 1 thenredis.call('hincrby','key','uuid:threadid',1)redis.call('expire','key',30)return 1elsereturn 0end

V2

if redis.call('exists','key') == 0 or redis.call('hexists','key','uuid:threadid') == 1 thenredis.call('hincrby','key','uuid:threadid',1)redis.call('expire','key',30)return 1elsereturn 0end

V3
在这里插入图片描述


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 elsereturn 0end

测试:
在这里插入图片描述
解锁lua脚本unlock:
设计思路:有锁且还是自己的锁。HEXISTS key uuid:ThreadlD
在这里插入图片描述
返回零,说明根本没有锁,程序块返回nil。不是零,说明有锁且是自己的锁,直接调用HINCRBY负一表示每次减个一,解锁一次。直到它变为零表示可以删除该锁Key,del锁key
在这里插入图片描述
上述设计修改为Lua脚本
V1

if redis.call('HEXISTS',lock,uuid:threadID) == 0 thenreturn nilelseif redis.call('HINCRBY',lock,uuid:threadID,-1) == 0 thenreturn redis.call('del',lock)else return 0end

V2

if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 thenreturn nilelseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 thenreturn redis.call('del',KEYS[1])elsereturn 0end

在这里插入图片描述
测试
在这里插入图片描述
将上述lua脚本整合进入微服务Java程序:
复原程序为初始无锁版

package com.atguigu.redislock.service;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;/*** @auther zzyy* @create 2022-10-22 15:14*/
@Service
@Slf4j
public class InventoryService
{@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Value("${server.port}")private String port;public String sale(){String retMessage = "";//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+"\t";System.out.println(retMessage);}else{retMessage = "商品卖完了,o(╥﹏╥)o";}return retMessage+"\t"+"服务端口号:"+port;}
}

新建RedisDistributedLock类并实现JUC里面的Lock接口
满足JUC里面AQS对Lock锁的接口规范定义来进行实现落地代码
结合设计模式开发属于自己的Redis分布式锁工具类:
lua脚本:加锁lock

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 elsereturn 0end

解锁unlock

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 0end

工厂设计模式引入:
通过实现UC里面的Lock接口,实现Redis分布式锁RedisDistributedLock

package com.atguigu.redislock.mylock;import cn.hutool.core.util.IdUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.support.collections.DefaultRedisList;
import org.springframework.stereotype.Component;import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;/*** @auther zzyy* @create 2022-10-18 18:32*/
//@Component 引入DistributedLockFactory工厂模式,从工厂获得而不再从spring拿到
public class RedisDistributedLock implements Lock
{private StringRedisTemplate stringRedisTemplate;private String lockName;//KEYS[1]private String uuidValue;//ARGV[1]private long   expireTime;//ARGV[2]public RedisDistributedLock(StringRedisTemplate stringRedisTemplate, String lockName){this.stringRedisTemplate = stringRedisTemplate;this.lockName = lockName;this.uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();//UUID:ThreadIDthis.expireTime = 30L;}@Overridepublic void lock(){tryLock();}@Overridepublic boolean tryLock(){try {tryLock(-1L,TimeUnit.SECONDS);} catch (InterruptedException e) {e.printStackTrace();}return false;}/*** 干活的,实现加锁功能,实现这一个干活的就OK,全盘通用* @param time* @param unit* @return* @throws InterruptedException*/@Overridepublic boolean tryLock(long time, TimeUnit unit) throws InterruptedException{if(time != -1L){this.expireTime = unit.toSeconds(time);}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("script: "+script);System.out.println("lockName: "+lockName);System.out.println("uuidValue: "+uuidValue);System.out.println("expireTime: "+expireTime);while (!stringRedisTemplate.execute(new DefaultRedisScript<>(script,Boolean.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime))) {TimeUnit.MILLISECONDS.sleep(50);}return true;}/***干活的,实现解锁功能*/@Overridepublic 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 = falseSystem.out.println("lockName: "+lockName);System.out.println("uuidValue: "+uuidValue);System.out.println("expireTime: "+expireTime);Long flag = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime));if(flag == null){throw new RuntimeException("This lock doesn't EXIST");}}//===下面的redis分布式锁暂时用不到=======================================//===下面的redis分布式锁暂时用不到=======================================//===下面的redis分布式锁暂时用不到=======================================@Overridepublic void lockInterruptibly() throws InterruptedException{}@Overridepublic Condition newCondition(){return null;}
}

InventoryService直接使用上面的代码设计,有什么问题

在这里插入图片描述
考虑扩展,本次是redis实现分布式锁,以后zookeeper、mysql实现呢??
引入工厂模式改造7.1版code
DistributedLockFactory

package com.atguigu.redislock.mylock;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;/*** @auther zzyy* @create 2022-10-18 18:53*/
@Component
public class DistributedLockFactory
{@Autowiredprivate StringRedisTemplate stringRedisTemplate;private String lockName;public Lock getDistributedLock(String lockType){if(lockType == null) return null;if(lockType.equalsIgnoreCase("REDIS")){lockName = "zzyyRedisLock";return new RedisDistributedLock(stringRedisTemplate,lockName);} else if(lockType.equalsIgnoreCase("ZOOKEEPER")){//TODO zookeeper版本的分布式锁实现return new ZookeeperDistributedLock();} else if(lockType.equalsIgnoreCase("MYSQL")){//TODO mysql版本的分布式锁实现return null;}return null;}
}

RedisDistributedLock

package com.atguigu.redislock.mylock;import cn.hutool.core.util.IdUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.support.collections.DefaultRedisList;
import org.springframework.stereotype.Component;import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;/*** @auther zzyy* @create 2022-10-18 18:32*/
//@Component 引入DistributedLockFactory工厂模式,从工厂获得而不再从spring拿到
public class RedisDistributedLock implements Lock
{private StringRedisTemplate stringRedisTemplate;private String lockName;//KEYS[1]private String uuidValue;//ARGV[1]private long   expireTime;//ARGV[2]public RedisDistributedLock(StringRedisTemplate stringRedisTemplate, String lockName){this.stringRedisTemplate = stringRedisTemplate;this.lockName = lockName;this.uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();//UUID:ThreadIDthis.expireTime = 30L;}@Overridepublic void lock(){tryLock();}@Overridepublic boolean tryLock(){try {tryLock(-1L,TimeUnit.SECONDS);} catch (InterruptedException e) {e.printStackTrace();}return false;}/*** 干活的,实现加锁功能,实现这一个干活的就OK,全盘通用* @param time* @param unit* @return* @throws InterruptedException*/@Overridepublic boolean tryLock(long time, TimeUnit unit) throws InterruptedException{if(time != -1L){this.expireTime = unit.toSeconds(time);}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("script: "+script);System.out.println("lockName: "+lockName);System.out.println("uuidValue: "+uuidValue);System.out.println("expireTime: "+expireTime);while (!stringRedisTemplate.execute(new DefaultRedisScript<>(script,Boolean.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime))) {TimeUnit.MILLISECONDS.sleep(50);}return true;}/***干活的,实现解锁功能*/@Overridepublic 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 = falseSystem.out.println("lockName: "+lockName);System.out.println("uuidValue: "+uuidValue);System.out.println("expireTime: "+expireTime);Long flag = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime));if(flag == null){throw new RuntimeException("This lock doesn't EXIST");}}//===下面的redis分布式锁暂时用不到=======================================//===下面的redis分布式锁暂时用不到=======================================//===下面的redis分布式锁暂时用不到=======================================@Overridepublic void lockInterruptibly() throws InterruptedException{}@Overridepublic Condition newCondition(){return null;}
}

lnventoryService使用工厂模式版

package com.atguigu.redislock.service;import ch.qos.logback.core.joran.conditional.ThenAction;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import com.atguigu.redislock.mylock.DistributedLockFactory;
import com.atguigu.redislock.mylock.RedisDistributedLock;
import lombok.extern.slf4j.Slf4j;
import org.omg.IOP.TAG_RMI_CUSTOM_MAX_STREAM_FORMAT;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
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.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;/*** @auther zzyy* @create 2022-10-12 17:04*/
@Service
@Slf4j
public class InventoryService
{@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Value("${server.port}")private String port;@Autowiredprivate DistributedLockFactory distributedLockFactory;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){inventoryNumber = inventoryNumber - 1;stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(inventoryNumber));retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber+"\t服务端口:" +port;System.out.println(retMessage);return retMessage;}retMessage = "商品卖完了,o(╥﹏╥)o"+"\t服务端口:" +port;}catch (Exception e){e.printStackTrace();}finally {redisLock.unlock();}return retMessage;}
}

可重入性测试重点
lnventoryService类新增可重入测试方法

package com.atguigu.redislock.service;import com.atguigu.redislock.mylock.DistributedLockFactory;
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 javax.annotation.Resource;
import java.util.concurrent.locks.Lock;/*** @auther zzyy* @create 2022-10-30 12:28*/
@Service
@Slf4j
public class InventoryService
{@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Value("${server.port}")private String port;@Autowiredprivate DistributedLockFactory distributedLockFactory;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+"\t";System.out.println(retMessage);testReEnter();}else{retMessage = "商品卖完了,o(╥﹏╥)o";}}catch (Exception e){e.printStackTrace();}finally {redisLock.unlock();}return retMessage+"\t"+"服务端口号:"+port;}private void testReEnter(){Lock redisLock = distributedLockFactory.getDistributedLock("redis");redisLock.lock();try{System.out.println("################测试可重入锁#######");}finally {redisLock.unlock();}}
}/**//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+"\t";System.out.println(retMessage);}else{retMessage = "商品卖完了,o(╥﹏╥)o";}*/

测试:http://localhost:7777/inventory/sale

在这里插入图片描述
ThreadlD一致了但是UUID不OK
引入工厂模式改造7.2版code
DistributedLockFactory

package com.atguigu.redislock.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;/*** @auther zzyy* @create 2022-10-23 22:40*/
@Component
public class DistributedLockFactory
{@Autowiredprivate StringRedisTemplate stringRedisTemplate;private String lockName;private String uuidValue;public DistributedLockFactory(){this.uuidValue = IdUtil.simpleUUID();//UUID}public Lock getDistributedLock(String lockType){if(lockType == null) return null;if(lockType.equalsIgnoreCase("REDIS")){lockName = "zzyyRedisLock";return new RedisDistributedLock(stringRedisTemplate,lockName,uuidValue);} else if(lockType.equalsIgnoreCase("ZOOKEEPER")){//TODO zookeeper版本的分布式锁实现return new ZookeeperDistributedLock();} else if(lockType.equalsIgnoreCase("MYSQL")){//TODO mysql版本的分布式锁实现return null;}return null;}
}

RedisDistributedLock

package com.atguigu.redislock.mylock;import cn.hutool.core.util.IdUtil;
import lombok.SneakyThrows;
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;/*** @auther zzyy* @create 2022-10-23 22:36*/
public class RedisDistributedLock implements Lock
{private StringRedisTemplate stringRedisTemplate;private String lockName;private String uuidValue;private long   expireTime;public RedisDistributedLock(StringRedisTemplate stringRedisTemplate, String lockName,String uuidValue){this.stringRedisTemplate = stringRedisTemplate;this.lockName = lockName;this.uuidValue = uuidValue+":"+Thread.currentThread().getId();this.expireTime = 30L;}@Overridepublic void lock(){this.tryLock();}@Overridepublic boolean tryLock(){try{return this.tryLock(-1L,TimeUnit.SECONDS);} catch (InterruptedException e) {e.printStackTrace();}return false;}@Overridepublic boolean tryLock(long time, TimeUnit unit) throws InterruptedException{if(time != -1L){expireTime = unit.toSeconds(time);}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))){try { TimeUnit.MILLISECONDS.sleep(60); } catch (InterruptedException e) { e.printStackTrace(); }}return true;}@Overridepublic 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";System.out.println("lockName: "+lockName+"\t"+"uuidValue: "+uuidValue);Long flag = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime));if(flag == null){throw new RuntimeException("没有这个锁,HEXISTS查询无");}}//=========================================================@Overridepublic void lockInterruptibly() throws InterruptedException{}@Overridepublic Condition newCondition(){return null;}
}

lnventoryService类新增可重入测试方法

package com.atguigu.redislock.service;import cn.hutool.core.util.IdUtil;
import com.atguigu.redislock.mylock.DistributedLockFactory;
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.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;/*** @auther zzyy* @create 2022-10-22 15:14*/
@Service
@Slf4j
public class InventoryService
{@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Value("${server.port}")private String port;@Autowiredprivate DistributedLockFactory distributedLockFactory;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;System.out.println(retMessage);this.testReEnter();}else{retMessage = "商品卖完了,o(╥﹏╥)o";}}catch (Exception e){e.printStackTrace();}finally {redisLock.unlock();}return retMessage+"\t"+"服务端口号:"+port;}private void testReEnter(){Lock redisLock = distributedLockFactory.getDistributedLock("redis");redisLock.lock();try{System.out.println("################测试可重入锁####################################");}finally {redisLock.unlock();}}
}

单机+并发+可重入性,测试通过

8.自动续期
确保redisLock过期时间大于业务执行时间的问题。Redis分布式锁如何续期?
CAP:Consistency(强一致性)、Availability(可用性)、Partition tolerance(分区容错性)
Redis集群是AP:redis异步复制造成的锁丢失,比如:主节点没来的及把刚刚set进来这条数据给从节点,master就挂了,从机上位但从机上无该数据
Zookeeper集群是CP:
在这里插入图片描述
故障:
在这里插入图片描述
Eureka集群是AP:
在这里插入图片描述
Nacos集群是AP:
在这里插入图片描述
加个钟,lua脚本:
在这里插入图片描述
8.0版新增自动续期功能
修改为V8.0版程序
del掉之前的lockName zzyyRedisLock
RedisDistributedLock:

package com.atguigu.redislock.mylock;import cn.hutool.core.util.IdUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.support.collections.DefaultRedisList;
import org.springframework.stereotype.Component;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;/*** @auther zzyy* @create 2022-10-18 18:32*/
public class RedisDistributedLock implements Lock
{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;}@Overridepublic void lock(){tryLock();}@Overridepublic boolean tryLock(){try {tryLock(-1L,TimeUnit.SECONDS);} catch (InterruptedException e) {e.printStackTrace();}return false;}/*** 干活的,实现加锁功能,实现这一个干活的就OK,全盘通用* @param time* @param unit* @return* @throws InterruptedException*/@Overridepublic boolean tryLock(long time, TimeUnit unit) throws InterruptedException{if(time != -1L){this.expireTime = unit.toSeconds(time);}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("script: "+script);System.out.println("lockName: "+lockName);System.out.println("uuidValue: "+uuidValue);System.out.println("expireTime: "+expireTime);while (!stringRedisTemplate.execute(new DefaultRedisScript<>(script,Boolean.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime))) {TimeUnit.MILLISECONDS.sleep(50);}this.renewExpire();return true;}/***干活的,实现解锁功能*/@Overridepublic 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 = falseSystem.out.println("lockName: "+lockName);System.out.println("uuidValue: "+uuidValue);System.out.println("expireTime: "+expireTime);Long flag = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime));if(flag == null){throw new RuntimeException("This lock doesn't EXIST");}}private void renewExpire(){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(){@Overridepublic void run(){if (stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime))) {renewExpire();}}},(this.expireTime * 1000)/3);}//===下面的redis分布式锁暂时用不到=======================================//===下面的redis分布式锁暂时用不到=======================================//===下面的redis分布式锁暂时用不到=======================================@Overridepublic void lockInterruptibly() throws InterruptedException{}@Overridepublic Condition newCondition(){return null;}
}

lnventoryService:

package com.atguigu.redislock.service;import cn.hutool.core.util.IdUtil;
import com.atguigu.redislock.mylock.DistributedLockFactory;
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.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;/*** @auther zzyy* @create 2022-10-22 15:14*/
@Service
@Slf4j
public class InventoryService
{@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Value("${server.port}")private String port;@Autowiredprivate DistributedLockFactory distributedLockFactory;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;System.out.println(retMessage);//暂停几秒钟线程,为了测试自动续期try { TimeUnit.SECONDS.sleep(120); } catch (InterruptedException e) { e.printStackTrace(); }}else{retMessage = "商品卖完了,o(╥﹏╥)o";}}catch (Exception e){e.printStackTrace();}finally {redisLock.unlock();}return retMessage+"\t"+"服务端口号:"+port;}private void testReEnter(){Lock redisLock = distributedLockFactory.getDistributedLock("redis");redisLock.lock();try{System.out.println("################测试可重入锁####################################");}finally {redisLock.unlock();}}
}

lnventoryService

package com.atguigu.redislock.service;import cn.hutool.core.util.IdUtil;
import com.atguigu.redislock.mylock.DistributedLockFactory;
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.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;/*** @auther zzyy* @create 2022-10-22 15:14*/
@Service
@Slf4j
public class InventoryService
{@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Value("${server.port}")private String port;@Autowiredprivate DistributedLockFactory distributedLockFactory;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;System.out.println(retMessage);//暂停几秒钟线程,为了测试自动续期try { TimeUnit.SECONDS.sleep(120); } catch (InterruptedException e) { e.printStackTrace(); }}else{retMessage = "商品卖完了,o(╥﹏╥)o";}}catch (Exception e){e.printStackTrace();}finally {redisLock.unlock();}return retMessage+"\t"+"服务端口号:"+port;}private void testReEnter(){Lock redisLock = distributedLockFactory.getDistributedLock("redis");redisLock.lock();try{System.out.println("################测试可重入锁####################################");}finally {redisLock.unlock();}}
}

记得去掉可重入测试testReEnter()。lnventoryService业务逻辑里面故意sleep一段时间测试自动续期

总结:
1.synchronized,单机版oK,上分布式死翘翘
2.nginx分布式微服务单机锁不行
3.取消单机锁,上redis分布式锁setnx
只加了锁,没有释放锁,出异常的话,可能无法释放锁,必须要在代码层面finally释放锁
宕机了,部署了微服务代码层面根本没有走到finally这块,没办法保证解锁,这个key没有被删除,
需要有lockKey的过期时间设定
为redis的分布式锁key,增加过期时间此外,还必须要setnx+过期时间必须同—行。必须规定只能自己删除自己的锁,你不能把别人的锁删除了,防止张冠李戴,1删2,2删3。unlock变为Lua脚本保证。锁重入,hset替代setnx+lock变为Lua脚本保证,自动续期


http://www.ppmy.cn/news/123198.html

相关文章

探索 Web 管理之路,OpenYurt 社区 UI/CLI SIG 正式启动

作者&#xff1a;陈璐、邓梁 背景 OpenYurt 是业界首个依托云原生技术体系、“零”侵入实现的智能边缘计算平台。它具有“云、边、端一体化”的全方位能力,可以帮助用户快速实现大规模边缘计算业务和异构算力的高效交付、运维及管理。 在前几个迭代的大版本中&#xff0c;Op…

线程方法 interrupt 可中断方法

interrupt 可中断方法 如下方法的调用会使得当前线程进入阻塞状态&#xff0c;而调用当前线程的interrupt方法&#xff0c;就可以打断阻塞。Object的wait方法。Object的wait&#xff08;long&#xff09;方法。Object的wait&#xff08;long&#xff0c;int&#xff09;方法。…

中国人民大学与加拿大女王大学金融硕士项目——在职读金融硕士,没想到收获这么多

随着社会经济的快速发展&#xff0c;金融专业的报考越来越受欢迎。近些年来&#xff0c;市场对于金融专业的高端人才需求不断增加。工作多年的金融人或有计划跳槽到金融领域的群体&#xff0c;都想通过业余时间充电以增强在金融行业的竞争力。在职读金融硕士有用吗&#xff1f;…

滑轮位置_除了万向跑步机,用“滑轮鞋”玩VR也有望缓解晕动症?

8月3日消息&#xff0c;为了找到在VR中移动的合适方法&#xff0c;从而缓解视觉辐辏调节冲突等症状&#xff0c;VR爱好者Finally Functional发明了一种可配合VR使用的&#xff0c;类似滑轮鞋的移动装置。 据青亭网了解&#xff0c;这款“VR滑轮鞋”原型每只采用多个双向电动轮&…

跑带宽度多少合适_家用跑步机跑带多宽合适

跑步机跑带宽度的合适与否对于跑步的舒适度也是至关重要的&#xff0c;但是也并不是说跑带的宽度越宽越好。虽然在理论上来说跑带可能都会觉得越宽就越好&#xff0c;但是前提是在保证跑步机整体质量和使用寿命的基础上决定的。也就是说跑带宽度越宽&#xff0c;对跑步机的整体…

跑带宽度多少合适_易跑科普:一般跑步机跑带宽度和长度多少合适?

在选购跑步机中&#xff0c;跑带至关重要也是大家比较容易忽视的地方&#xff0c;在先前已经有大家讲解过跑步机跑带宽度多少适宜&#xff0c;有些伙伴看完表示还是有所疑惑&#xff0c;今天再次给大家强调下跑步机跑带宽度和长度多少合适&#xff0c;为广大易跑用户答疑解惑。…

跑带宽度多少合适_家用跑步机跑带长宽选择多少合适?

我们说选择跑步机&#xff0c;除了价格因素外&#xff0c;配置是我们要重点考虑的&#xff0c;特别是马达、减震、跑步机大小等。其中跑步机大小分为跑带宽度和长度&#xff0c;它的尺寸直接影响跑步时的舒适感&#xff0c;如果带宽过小&#xff0c;影响手臂摆动不说&#xff0…

跑带宽度多少合适_有多少人知道跑步机跑带宽度和长度多少合适?

一般跑步机跑带宽度和长度多少合适? 在购买跑步机的时候&#xff0c;很多人会忽略跑带这个地方&#xff0c;其实对于跑步的人群来说&#xff0c;是至关重要的&#xff0c;在先前汉臣小编就跟大家讲解过一些跑步机宽带的相关问题&#xff0c;但有些人看完表示仍有疑惑&#xff…