Redis 分布式锁
面试题:
- Redis除了拿来做缓存,你还见过基于Redis的什么用法?
1.数据共享,分布式Session
2.分布式锁
3.全局ID
4.计算器、点赞
5.位统计
6.购物车
7.轻量级消息队列:list、stream
8.抽奖
9.点赞、签到、打卡
10.差集交集并集,用户关注、可能认识的人,推荐模型
11.热点新间、热搜排行榜
- Redis做分布式锁的时候有需要注意的问题?
- 你们公司自己实现的分布式锁是否用的setnx命令实现?这个是最合适的吗?你如何考虑分布式锁的可重入问题?
- 如果是Redis是单点部署的,会带来什么问题?那你准备怎么解决单点问题呢?
- Redis集群模式下,比如主从模式,CAP方面有没有什么问题呢?
- 那你简单的介绍一下Redlock吧?你简历上写redisson,你谈谈
- Redis分布式锁如何续期?看门狗知道吗?
分布式锁需要具备的条件和刚需
独占性
OnlyOne,任何时刻只能有且仅有一个线程持有
高可用
若redis集群环境下,不能因为某一个节点挂了而出现获取锁和释放锁失败的情况;高并发请求下,依旧性能OK好使
防死锁
杜绝死锁,必须有超时控制机制或者撤销操作,有个兜底终止跳出方案
不乱抢
防止张冠李戴,不能私下unlock别人的锁,只能自己加锁自己释放,自己约的锁含着泪也要自己解
重入性
同一个节点的同一个线程如果获得锁之后,它也可以再次获取这个锁
分布式锁
官网:set 命令
使用set进行占锁,删掉key代表锁释放。
setnx key value
注:setnx+expire不安全,两条命令非原子性的。
set key value [EX seconds] [PX milliseconds] [NX|XX]
重点:JUC中AQS锁的规范落地参考+可重入锁考虑+Lua脚本+Redist命令一步步实现分布式锁
案例
V1.0 版本(基础案例)
使用场景:多个服务间保证同一时刻同一时间段内同一用户只能有一个请求(防止关键业务出现并发攻击)。
1、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 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>
2、ymal文件
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
3、主启动类
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication
public class RedisDistributedLockApp {public static void main(String[] args) {SpringApplication.run(RedisDistributedLockApp.class,args);}
}
4、配置类
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;@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();}}
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;@Configuration
public class RedisConfig {@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;}
}
5、业务类
import cn.hutool.core.util.IdUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;@Service
@Slf4j
public class InventoryService {@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Value("${server.port}")private String port;public String sale() {String retMessage = "";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 {// 释放操作}return retMessage+"\t"+"服务端口号:"+port;}
}
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;@RestController
@Api(tags = "redis分布式锁测试")
public class InventoryController {@Autowiredprivate InventoryService inventoryService;@ApiOperation("扣减库存,一次卖一个")@GetMapping(value = "/inventory/sale")public String sale() {return inventoryService.sale();}
}
丝袜哥测试地址:http://localhost:7777/swagger-ui.html#/
V2.0 版本
使用 synchronized 或者 lock 进行加锁操作
import cn.hutool.core.util.IdUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;@Service
@Slf4j
public class InventoryService {@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端口,使用 nginx 轮训访问 7777 和 8888 服务,此时就会出现超卖现象。lock 只针对当前服务有用,锁不了其他服务。
Nginx配置负载均衡
1、常用命令
检查nginx.conf文件合法性:
nginx -t -c ./conf/nginx.conf
检查nginx服务是否启动:
tasklist /fi "imagename eq nginx.exe"
启动nginx:
start nginx 或 ./nginx
重新加载:
nginx -s reload
停止服务:
nginx -s stop
2、配置文件
服务测试
启动 7777 和 8888 服务,通过Nginx访问,你的Linux服务器地址lP,反向代理+负裁均衡,可以点击看到效果,一边一个,默认轮询:http://192.168.0.185/inventory/sale
使用 jmeter 进行压测
http请求:
测试结果:
异常:76号商品被卖出2次,出现超卖故章现象。
结论
在单机环境下,可以使用synchronized或Lock来实现。
但是在分布式系统中,因为竞争的线程可能不在同一个节点上(同一个jvm中),所以需要一个让所有进程都能访问到的锁来实现(比如redis或者zookeeper来构建)。
不同进程jvm层面的锁就不管用了,那么可以利用第三方的一个组件,来获取锁,未获取到锁,则阻塞当前想要运行的线程。
V3.1 版本
使用递归进行重试
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;@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;}
}
结果:
1、手工测试OK,使用Jmeter压测5000也OK
2、递归是一种思想没错,但是容号导致StackOverflowError,不太推荐,需进一步完善
V3.2 版本
使用多线程判断想想JUC里面的虚假唤醒,用while替代if,用自旋替代送归重试。
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;@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;}
}
问题:部署了微服务的Java程序机器挂了,代码层面根本没有走到finallyi这块,没办法保证解锁(无过期时间该key一直存在),这个kev没有被删除,需要加入一个过期时间限定key。
V4.1 版本
......
while(!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue)) {//暂停20毫秒,进行递归重试.....try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
}
......
stringRedisTemplate.expire(key,30L,TimeUnit.SECONDS);
......
结论:虽然加了过期时间,但是设置key+过期时间分开了,不具备原子性。
V4.2 版本
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;@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
结论:加锁和过期时间设置必须同一行,保证原子性。
V5.0 版本
4.2版本出现的问题:实际业务处理时间如果超过了默认设晋key的过期时间怎么办??张冠李戴,别除了别人的锁(只能自己除自己的,不许动别人的)
改进:
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;@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;}
}
V6.0 版本(小厂够用)
上面5.0版本出现的问题:finally块的判断+del删除除操作不是原子性的。
解决方案:启用lua脚本编写redis分布式锁判断+删除判断代码。
原理:Redis调用Lua本通过eval命令保证代码执行的原子性,直接用return返回脚本执行后的结果值。
Lua 脚本
官网:https://redis.io/docs/manual/patterns/distributed-locks/
脚本格式:
eval luascript numkeys [key [key ]][arg [arg ...]]
案例一:输出文字
案例二:组合命令
案例三:执行命令
案例四:条件判断
使用格式如下
eval "if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end" 1 zzyyRedisLock 1111-2222-3333
解决方式
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;@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 说明
V7.0 版本(可重入锁)
6.0版本:while判断并自旋重试获取锁+setx含自然过期时间+ua脚本官网除锐命令。那么问题来了:如何兼顾锁的可重入性问题?
一个靠谱分布式锁需要具备的条件和刚需:独占性、高可用、防死锁、不乱抢、重入性
可重入锁
说明:可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。如果是1个有 synchronized 修饰的递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚。所以Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
“可重入锁”这四个字分开来解释:
可:可以。
重:再次。
入:进入。
锁:同步锁。进入什么:进入同步域(即同步代码快/方法或显式锁锁定的代码)
一句话:一个线程中的多个流程可以获取同一把锁,持有这把同步锁句以再次进入。自己可以获取自己的内部锁。
可重入锁种类:
第一种:隐式锁(即synchronized关键字使用的锁)默认是可重入锁。
指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。
简单的来说就是:在一个synchronized修饰的方法或代码块的内部调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁的。
与可重入锁相反,不可重入锁不可递归调用,递归调用就发生死锁。
同步块:
public class ReEntryLockDemo {public static void main(String[] args) {final Object objectLockA = new Object();new Thread(() -> {synchronized (objectLockA) {System.out.println("-----外层调用");synchronized (objectLockA) {System.out.println("-----中层调用");synchronized (objectLockA) {System.out.println("-----内层调用");}}}},"a").start();}
}
同步方法:
/*** 在一个Synchronized修饰的方法或代码块的内部调用本类的其他Synchronized修饰的方法或代码块时,是永远可以得到锁的*/
public class ReEntryLockDemo {public synchronized void m1() {System.out.println("-----m1");m2();}public synchronized void m2() {System.out.println("-----m2");m3();}public synchronized void m3() {System.out.println("-----m3");}public static void main(String[] args) {ReEntryLockDemo reEntryLockDemo = new ReEntryLockDemo();reEntryLockDemo.m1();}
}
实现原理:
每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么 Java 虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。
第二种:显式锁(即Lock)也有ReentrantLock这样的可重入锁。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;/*** 在一个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();}
}
备注:一般而言,你lock了几次就要unlock几次。
Redis 可重入锁数据类型
使用<key,<key,value>>结构:Map<String,Map<Object,Object>>
hset zzyyRedisLock 29f0ee01ac77414fb8b0861271902a94:1
案例:
hset key field value
hset redis锁名字(zzyyRedisLock) 某个请求线程的UUID+ThreadID 加锁的次数
总结:setnx,只能解决有无的问题,够用但是不完美;hset,不但解决有无,还解决可重入问题。
Lua脚本
上面版本扣减库存的核心代码:(保证原子性)
实现要求:(lua脚本实现过程分析)
一、加锁lua脚本(其实就是 lock)
步骤:
1、先断redis分布式锁这个key是否存在 :EXISTS key
2、返回零说明不存在,hset新建当前线程属于自己的锁BY UUID:ThreadID
3、返回壹说明已经有锁,需进一步判断是不是当前线程自己的:HEXISTS key uuid:ThreadID
命令:HEXISTS zzyyRedisLock 0c90d37cb6ec42268861b3d739f8b3a8:1
3.1、返回零说明不是自己的
3.2、返回壹说明是自己的锁,自增1次表示重入
命令:HINCRBY key field increment
案例:HINCRBY zzyyRedisLock 0c90d37cb6ec42268861b3d739f8b3a8:1 1
根据以上步骤,得出lua脚本:
if redis.call('exists','key') == 0 then
redis.call('hset','key','uuid:threadid',1)
redis.call('expire','key',30)
return 1
elseif redis.call('hexists','key','uuid:threadid') == 1 then
redis.call('hincrby','key','uuid:threadid',1)
redis.call('expire','key',30)
return 1
else
return 0
end
以上相同部分是否可以替换处理???
答案:hincrby命令可否替代hset命令。(自增命令,没有key会创建key)
优化脚本:
if redis.call('exists','key') == 0 or redis.call('hexists','key','uuid:threadid') == 1 then
redis.call('hincrby','key','uuid:threadid',1)
redis.call('expire','key',30)
return 1
else
return 0
end
替换参数:
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
参数说明:
测试加锁lua脚本:
EVAL "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" 1 zzyyRedisLock 0c90d37cb6ec42268861b3d739f8b3a8:1 30
HGET zzyyRedisLock 0c90d37cb6ec42268861b3d739f8b3a8:1
二、解锁Lua脚本(其实就是 unlock)
步骤:
1、先判断redis分布式锁这个key是否存在:EXISTS key
2、返回零,说明根本没有锁,程序块返回nil
3、不是零,说明有锁且是自己的锁,直接调用 HINCRBY -1 表示每次减个一,解领一次,直到它变为零表示可以除该锁key,del锁key
根据以上步骤得出解锁lua脚本:
if redis.call('HEXISTS',lock,uuid:threadID) == 0 then
return nil
elseif redis.call('HINCRBY',lock,uuid:threadID,-1) == 0 then
return redis.call('del',lock)
else
return 0
end
替换参数:
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
调试命令:
eval "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" 1 zzyyRedisLock 2f586ae740a94736894ab9d51880ed9d:1
测试解锁lua脚本:
整合Java程序
原程序:(初始无锁版)
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;@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类并实现UUC里面的Lock接口,满足JUC里面AQS对Lock锁的接口规范定义来进行实现落地代码。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.locks.Lock;@Component
public class DistributedLockFactory {@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;}
}
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;//@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使用工厂模式版:
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;
@Service
@Slf4j
public class InventoryService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String port;
@Autowired
private 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;
}
}
可重入性加锁
测试代码改造:InventoryService 类
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;
@Service
@Slf4j
public class InventoryService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String port;
@Autowired
private 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();
}
}
}
测试结果:ThreadID一致了但是UUID不OK。
改造代码:
import cn.hutool.core.util.IdUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.locks.Lock;
@Component
public class DistributedLockFactory {
@Autowired
private StringRedisTemplate stringRedisTemplate;
private String lockName;
private String uuidValue;
public DistributedLockFactory() {
this.uuidValue = IdUtil.simpleUUID();//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;
}
}
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;
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;
}
@Override
public void lock() {
this.tryLock();
}
@Override
public boolean tryLock() {
try {
return this.tryLock(-1L,TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
return false;
}
@Override
public 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;
}
@Override
public void unlock() {
String script =
"if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then " +
"return nil " +
"elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then " +
"return redis.call('del',KEYS[1]) " +
"else " +
"return 0 " +
"end";
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查询无");
}
}
//=========================================================
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public Condition newCondition() {
return null;
}
}
InventoryService类新增可重入测试方法:
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;
@Service
@Slf4j
public class InventoryService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String port;
@Autowired
private 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();
}
}
}
结果:单机+并发+可重入性,测试通过
V8.0版本(自动续期)
思考:确保redisLock过期时间大于业务执行时间的问题,Redis分布式如何续续期?
什么是CAP?
- C(一致性):所有的节点上的数据时刻保持同步
- A(可用性):每个请求都能接受到一个响应,无论响应成功或失败
- P(分区容错):系统应该能持续提供服务,即使系统内部有消息丢失(分区)
- CA without P:如果不要求P(不允许分区),则C(强一致性)和A(可用性)是可以保证的。但其实分区不是你想不想的问题,而是始终会存在,因此CA的系统更多的是允许分区后各子系统依然保持CA。
- CP without A:如果不要求A(可用),相当于每个请求都需要在Server之间强一致,而P(分区)会导致同步时间无限延长,如此CP也是可以保证的。很多传统的数据库分布式事务都属于这种模式。
- AP wihtout C:要高可用并允许分区,则需放弃一致性。一旦分区发生,节点之间可能会失去联系,为了高可用,每个节点只能用本地数据提供服务,而这样会导致全局数据的不一致性。现在众多的NoSQL都属于此类。
Redis集群是AP
redis异步复制造成的锁丢失,比如:主节点没来的及把刚刚set进来这条数据给从节点,master就挂了,从机上位但从机上无该数据。
Zookeeper集群是CP
故障问题:
Eureka集群是AP
Nacos集群是AP
lua代码(加钟)
脚本测试:
hset zzyyRedisLock 111122223333:11 3
EXPIRE zzyyRedisLock 30
ttl zzyyRedisLock
。。。。。
eval "if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 1 then return redis.call('expire',KEYS[1],ARGV[2]) else return 0 end" 1 zzyyRedisLock 111122223333:11 30
ttl zzyyRedisLock
lua脚本:
//==============自动续期
if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 1 then
return redis.call('expire',KEYS[1],ARGV[2])
else
return 0
end
自动续期
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;
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;
}
@Override
public void lock() {
tryLock();
}
@Override
public boolean tryLock() {
try {tryLock(-1L,TimeUnit.SECONDS);} catch (InterruptedException e) {e.printStackTrace();}
return false;
}
/**
* 干活的,实现加锁功能,实现这一个干活的就OK,全盘通用
* @param time
* @param unit
* @return
* @throws InterruptedException
*/
@Override
public 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;
}
/**
*干活的,实现解锁功能
*/
@Override
public void unlock() {
String script =
"if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then " +
" return nil " +
"elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then " +
" return redis.call('del',KEYS[1]) " +
"else " +
" return 0 " +
"end";
// nil = false 1 = true 0 = false
System.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() {
@Override
public 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分布式锁暂时用不到=======================================
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public Condition newCondition() {
return null;
}
}
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;
@Service
@Slf4j
public class InventoryService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String port;
@Autowired
private 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();
}
}
}
总结流程
1、使用 synchronized 关键字或使用jdk中Lock,单机版OK,上分布式死翅翘
2、nginx分布式微服务,单机锁不行
3、取消单机锁,上redis分布式锁setnx
3.1、只加了锁,没有释放锁,出异常的话,可能无法释放锁,必须要在代码层面finally释放锁
3.2、宕机了,部署了微服务代码层面根本没有走到finally这块,没办法保证解锁,这个key没有被删除,需要有lockKey的过期时间设定
4、为redis的分布式锁key,增加过期时间,此外,还必须要setnx+过期时间同一行
4.1、必须规定只能自己删除自己的锁,你不能把别人的锁删除了,防止张冠李戴,1别2,2删3的情况
5、unlock变为lua脚本保证原子性
6、锁重入,hset替代setnx+lock变为Lua脚本保证
7、锁的自动续期
相关文章:

Redis 分布式锁
面试题: Redis除了拿来做缓存,你还见过基于Redis的什么用法? 1.数据共享,分布式Session 2.分布式锁 3.全局ID 4.计算器、点赞 5.位统计 6.购物车 7.轻量级消息队列:list、stream 8.抽奖 9.点赞、签到、打卡 10.差集交集…...

水循环原理VR实景教学课件开发
日本核污水排海让人们越来越重视海洋大气层水循环的安全,水循环是一个周而复始、循环往复的动态过程,为了将水循环过程以形象、生动地形式展示出来,水循环VR全景动态演示逐渐受到大家青睐。 传统的水循环教育方式通常是通过图片、动画或实地考…...

[小尾巴 UI 组件库] 全屏响应式轮播背景图(基于 Vue 3 与 Element Plus)
文章归档于:https://www.yuque.com/u27599042/row3c6 组件库地址 npm:https://www.npmjs.com/package/xwb-ui?activeTabreadme小尾巴 UI 组件库源码 gitee:https://gitee.com/tongchaowei/xwb-ui小尾巴 UI 组件库测试代码 gitee:…...

关于ESP32S3无法识别到端口问题
前言 (1)因为实习问题,需要使用ESP32BOX进行二次开发。一般来说,接触一款MCU,3天上手是基本操作。但是对于乐鑫的芯片,环境搭建是真的折磨人(苦笑),而且官方文档几乎没有…...
如何判断bug是前端bug还是后端bug
1.前后端bug 特征: 前端主要负责显示数据,后端主要负责处理数据、存储数据,前后端主要通过接口进行数据交换。 前端bug的特征: 界面显示类问题:如文字大小不一,控件颜色不搭,控件不整齐&#x…...
EasyExcel填充数据EasyExcel填充数据流下载 easyexcel填充excel下载 easyexcel填充
EasyExcel填充数据EasyExcel填充数据流下载 easyexcel填充excel下载 easyexcel填充 1、填充数据然后将文件输出给浏览器 1、填充数据然后将文件输出给浏览器 官网地址 官网的demo填充生成的是file文件, 实际需求用的最多的是 填充完数据将Excel输出给浏览器 填充模版Excel文…...

精通数据集成:轻易云数据集成平台应用实战与技术内幕
企业系统中数据交互的重要性不言而喻。一个系统如果孤立运行,即使积累了海量数据,仍然是一座信息孤岛。另一方面,一个积极参与数据交互的系统,成为各系统之间的“交际花”,具备中台的性质。 然而,大多数情况…...

“2023中国数字化服务之选”榜单发布,谷器数据荣耀登榜!
9月7日,行业权威媒体Internet Deep联合eNET研究院、德本咨询重磅发布了“2023中国数字化服务之选”榜单,经专家评审认定,谷器数据凭借领先的综合技术实力与产品应用创新能力荣耀登榜! 谷器数据生产制造数字化服务根据客户整体需求…...

iisfastchi漏洞复现
1.查看版本 2.在路径中写入 php脚本 发现使用不了 3.环境搭建 结局打开 把限制打开...

虚拟机Ubuntu操作系统常用终端命令(详细解释+详细演示)
虚拟机Ubuntu操作系统常用终端命令 本篇讲述了Ubuntu操作系统常用的三个功能,即归档,软链接和用户管理方面的相关知识。希望能够得到大家的支持。 文章目录 虚拟机Ubuntu操作系统常用终端命令二、使用步骤1.归档1.1创建档案包1.2还原档案包1.3归档并压缩…...

【力扣每日一题】2023.9.12 课程表Ⅳ
目录 题目: 示例: 分析: 代码: 题目: 示例: 分析: 今天是课程表系列题目的最后一题,因为我在题库里找不到课程表5了,所以今天的每日一题就是最后一个课程表了。 题…...

CentOS 安装HTTP代理服务器 Tinyproxy
Tinyproxy是一个小型的基于GPL的HTTP/SSL代理程序,非常适合小型网络而且便于快速部署。这个代理程序最大的优点就是占用系统资源比较少。这里使用的系统为CentOS7.6,可以直接 yum 方式安装。 yum install tinyproxy -y 如果提示找不到安装包࿰…...

PHPWord 模板输出checkbox复选框和checked已勾选状态,以及 模板替换时数据如何分行
PHPWord 模板输出checkbox复选框和checked已勾选状态,以及 模板替换时数据如何分行 期望目标 和 模板配置复选框代码数据分行显示在使用PHPWORD模板替换时,经常会有 动态渲染选择项 和 选择项需要被勾选的要求,以及动态数据分行显示等要求。 本文根据 空复选框、勾选复选框…...

vue学习之 v-for key
v-for key Vue使用 v-for渲染的元素列表时,它默认使用“就地更新”的策略。如果数据项的顺序被改变,Vue 将不会移动 DOM元素来匹配数据项的顺序,而是就地更新每个元素。创建 demo9.html,内容如下 <!DOCTYPE html> <html lang"…...

ARM接口编程—IIC总线(exynos 4412平台)
IIC总线简介 IIC总线是Philips公司在八十年代初推出的一种串行、半双工总线 主要用于近距离、低速的芯片之间的通信;IIC总线有两根双向的信号线一根数据线SDA用于收发数据,一根时钟线SCL用于通信双方时钟的同步;IIC总线硬件结构简单ÿ…...
ReactNative进阶(二十一)开源插件 react-native-device-info 获取设备信息
文章目录 一、前言二、拓展阅读 一、前言 项目开发过程中,需要获取设备信息,例如获取设备名称。可通过使用开源的第三方组件react-native-device-info,该组件适用于iOS和Android双平台。 在ReactNative项目中可通过npm命令下载 react-nativ…...
MySql学习笔记05——DML
DML 插入数据insert insert into 表名(字段名1,字段名2,字段名3...)values(值1,值2,值3);字段名和值要一一对应(顺序对应,数据类型对应) insert into t_student(no,name,sex,age,…...

halcon对图片进行处理基础
实例图片 C:\Users\Public\Documents\MVTec\HALCON-19.11-Progress\examples\images*读取图片 read_image (Image1, D:/c/image/1.png) *读取大小 get_image_size(Image1,Width, Height) *创建窗口 dev_close_window() dev_open_window(0, 0, Width, Height, black, WindowHan…...
element-ui在vue中如何实现校验两个复选框至少选择一个!
整体进行验证 想验证至少选择一个复选框,可以将这两个复选框视为一个整体进行验证。在 Element UI 中,可以使用自定义验证规则来实现这一点。 你可以在 rules 对象中定义一个自定义的验证规则来检查 isCheck1 和 isCheck2 至少有一个被选中。 以下是一…...

DeepinV20/Ubuntu安装postgresql方法
首先,建议看一下官方的安装文档PostgreSQL: Linux downloads (Ubuntu) PostgreSQL Apt Repository 简单的说,就是Ubuntu下的Apt仓库,可以用来安装任何支持版本的PgSQL。 If the version included in your version of Ubuntu is not the one…...

Flask RESTful 示例
目录 1. 环境准备2. 安装依赖3. 修改main.py4. 运行应用5. API使用示例获取所有任务获取单个任务创建新任务更新任务删除任务 中文乱码问题: 下面创建一个简单的Flask RESTful API示例。首先,我们需要创建环境,安装必要的依赖,然后…...

基于uniapp+WebSocket实现聊天对话、消息监听、消息推送、聊天室等功能,多端兼容
基于 UniApp + WebSocket实现多端兼容的实时通讯系统,涵盖WebSocket连接建立、消息收发机制、多端兼容性配置、消息实时监听等功能,适配微信小程序、H5、Android、iOS等终端 目录 技术选型分析WebSocket协议优势UniApp跨平台特性WebSocket 基础实现连接管理消息收发连接…...
【ROS】Nav2源码之nav2_behavior_tree-行为树节点列表
1、行为树节点分类 在 Nav2(Navigation2)的行为树框架中,行为树节点插件按照功能分为 Action(动作节点)、Condition(条件节点)、Control(控制节点) 和 Decorator(装饰节点) 四类。 1.1 动作节点 Action 执行具体的机器人操作或任务,直接与硬件、传感器或外部系统…...
Unit 1 深度强化学习简介
Deep RL Course ——Unit 1 Introduction 从理论和实践层面深入学习深度强化学习。学会使用知名的深度强化学习库,例如 Stable Baselines3、RL Baselines3 Zoo、Sample Factory 和 CleanRL。在独特的环境中训练智能体,比如 SnowballFight、Huggy the Do…...

如何理解 IP 数据报中的 TTL?
目录 前言理解 前言 面试灵魂一问:说说对 IP 数据报中 TTL 的理解?我们都知道,IP 数据报由首部和数据两部分组成,首部又分为两部分:固定部分和可变部分,共占 20 字节,而即将讨论的 TTL 就位于首…...
【生成模型】视频生成论文调研
工作清单 上游应用方向:控制、速度、时长、高动态、多主体驱动 类型工作基础模型WAN / WAN-VACE / HunyuanVideo控制条件轨迹控制ATI~镜头控制ReCamMaster~多主体驱动Phantom~音频驱动Let Them Talk: Audio-Driven Multi-Person Conversational Video Generation速…...

AI病理诊断七剑下天山,医疗未来触手可及
一、病理诊断困局:刀尖上的医学艺术 1.1 金标准背后的隐痛 病理诊断被誉为"诊断的诊断",医生需通过显微镜观察组织切片,在细胞迷宫中捕捉癌变信号。某省病理质控报告显示,基层医院误诊率达12%-15%,专家会诊…...
Java求职者面试指南:Spring、Spring Boot、MyBatis框架与计算机基础问题解析
Java求职者面试指南:Spring、Spring Boot、MyBatis框架与计算机基础问题解析 一、第一轮提问(基础概念问题) 1. 请解释Spring框架的核心容器是什么?它在Spring中起到什么作用? Spring框架的核心容器是IoC容器&#…...

AI+无人机如何守护濒危物种?YOLOv8实现95%精准识别
【导读】 野生动物监测在理解和保护生态系统中发挥着至关重要的作用。然而,传统的野生动物观察方法往往耗时耗力、成本高昂且范围有限。无人机的出现为野生动物监测提供了有前景的替代方案,能够实现大范围覆盖并远程采集数据。尽管具备这些优势…...

Linux nano命令的基本使用
参考资料 GNU nanoを使いこなすnano基础 目录 一. 简介二. 文件打开2.1 普通方式打开文件2.2 只读方式打开文件 三. 文件查看3.1 打开文件时,显示行号3.2 翻页查看 四. 文件编辑4.1 Ctrl K 复制 和 Ctrl U 粘贴4.2 Alt/Esc U 撤回 五. 文件保存与退出5.1 Ctrl …...