• 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文件

    1. "1.0" encoding="UTF-8"?>
    2. <project xmlns="http://maven.apache.org/POM/4.0.0"
    3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    4. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    5. <modelVersion>4.0.0modelVersion>
    6. <groupId>com.atguigu.redislockgroupId>
    7. <artifactId>redis_distributed_lock2artifactId>
    8. <version>1.0-SNAPSHOTversion>
    9. <parent>
    10. <groupId>org.springframework.bootgroupId>
    11. <artifactId>spring-boot-starter-parentartifactId>
    12. <version>2.6.12version>
    13. <relativePath/>
    14. parent>
    15. <properties>
    16. <project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
    17. <maven.compiler.source>8maven.compiler.source>
    18. <maven.compiler.target>8maven.compiler.target>
    19. <lombok.version>1.16.18lombok.version>
    20. properties>
    21. <dependencies>
    22. <dependency>
    23. <groupId>org.springframework.bootgroupId>
    24. <artifactId>spring-boot-starter-webartifactId>
    25. dependency>
    26. <dependency>
    27. <groupId>org.springframework.bootgroupId>
    28. <artifactId>spring-boot-starter-data-redisartifactId>
    29. dependency>
    30. <dependency>
    31. <groupId>org.apache.commonsgroupId>
    32. <artifactId>commons-pool2artifactId>
    33. dependency>
    34. <dependency>
    35. <groupId>io.springfoxgroupId>
    36. <artifactId>springfox-swagger2artifactId>
    37. <version>2.9.2version>
    38. dependency>
    39. <dependency>
    40. <groupId>io.springfoxgroupId>
    41. <artifactId>springfox-swagger-uiartifactId>
    42. <version>2.9.2version>
    43. dependency>
    44. <dependency>
    45. <groupId>org.springframework.bootgroupId>
    46. <artifactId>spring-boot-starter-testartifactId>
    47. <scope>testscope>
    48. dependency>
    49. <dependency>
    50. <groupId>org.projectlombokgroupId>
    51. <artifactId>lombokartifactId>
    52. <version>${lombok.version}version>
    53. <optional>trueoptional>
    54. dependency>
    55. <dependency>
    56. <groupId>cn.hutoolgroupId>
    57. <artifactId>hutool-allartifactId>
    58. <version>5.8.8version>
    59. dependency>
    60. dependencies>
    61. <build>
    62. <plugins>
    63. <plugin>
    64. <groupId>org.springframework.bootgroupId>
    65. <artifactId>spring-boot-maven-pluginartifactId>
    66. plugin>
    67. plugins>
    68. build>
    69. project>

    2、ymal文件

    1. server.port=7777
    2. spring.application.name=redis_distributed_lock
    3. # ========================swagger2=====================
    4. # http://localhost:7777/swagger-ui.html
    5. swagger2.enabled=true
    6. spring.mvc.pathmatch.matching-strategy=ant_path_matcher
    7. # ========================redis单机=====================
    8. spring.redis.database=0
    9. spring.redis.host=192.168.111.185
    10. spring.redis.port=6379
    11. spring.redis.password=111111
    12. spring.redis.lettuce.pool.max-active=8
    13. spring.redis.lettuce.pool.max-wait=-1ms
    14. spring.redis.lettuce.pool.max-idle=8
    15. spring.redis.lettuce.pool.min-idle=0

    3、主启动类

    1. import org.springframework.boot.SpringApplication;
    2. import org.springframework.boot.autoconfigure.SpringBootApplication;
    3. @SpringBootApplication
    4. public class RedisDistributedLockApp {
    5. public static void main(String[] args) {
    6. SpringApplication.run(RedisDistributedLockApp.class,args);
    7. }
    8. }

    4、配置类

    1. import org.springframework.beans.BeansException;
    2. import org.springframework.beans.factory.annotation.Value;
    3. import org.springframework.beans.factory.config.BeanPostProcessor;
    4. import org.springframework.context.annotation.Bean;
    5. import org.springframework.context.annotation.Configuration;
    6. import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping;
    7. import springfox.documentation.builders.ApiInfoBuilder;
    8. import springfox.documentation.builders.PathSelectors;
    9. import springfox.documentation.builders.RequestHandlerSelectors;
    10. import springfox.documentation.service.ApiInfo;
    11. import springfox.documentation.spi.DocumentationType;
    12. import springfox.documentation.spring.web.plugins.Docket;
    13. import springfox.documentation.spring.web.plugins.WebMvcRequestHandlerProvider;
    14. import springfox.documentation.swagger2.annotations.EnableSwagger2;
    15. import java.time.LocalDateTime;
    16. import java.time.format.DateTimeFormatter;
    17. import java.util.List;
    18. import java.util.stream.Collectors;
    19. @Configuration
    20. @EnableSwagger2
    21. public class Swagger2Config {
    22. @Value("${swagger2.enabled}")
    23. private Boolean enabled;
    24. @Bean
    25. public Docket createRestApi() {
    26. return new Docket(DocumentationType.SWAGGER_2)
    27. .apiInfo(apiInfo())
    28. .enable(enabled)
    29. .select()
    30. .apis(RequestHandlerSelectors.basePackage("com.atguigu.redislock")) //你自己的package
    31. .paths(PathSelectors.any())
    32. .build();
    33. }
    34. private ApiInfo apiInfo() {
    35. return new ApiInfoBuilder()
    36. .title("springboot利用swagger2构建api接口文档 "+"\t"+ DateTimeFormatter.ofPattern("yyyy-MM-dd").format(LocalDateTime.now()))
    37. .description("springboot+redis整合")
    38. .version("1.0")
    39. .termsOfServiceUrl("https://www.baidu.com/")
    40. .build();
    41. }
    42. }
    1. import org.springframework.context.annotation.Bean;
    2. import org.springframework.context.annotation.Configuration;
    3. import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
    4. import org.springframework.data.redis.core.RedisTemplate;
    5. import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
    6. import org.springframework.data.redis.serializer.StringRedisSerializer;
    7. @Configuration
    8. public class RedisConfig {
    9. @Bean
    10. public RedisTemplate redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {
    11. RedisTemplate redisTemplate = new RedisTemplate<>();
    12. redisTemplate.setConnectionFactory(lettuceConnectionFactory);
    13. //设置key序列化方式string
    14. redisTemplate.setKeySerializer(new StringRedisSerializer());
    15. //设置value的序列化方式json
    16. redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
    17. redisTemplate.setHashKeySerializer(new StringRedisSerializer());
    18. redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
    19. redisTemplate.afterPropertiesSet();
    20. return redisTemplate;
    21. }
    22. }

    5、业务类

    1. import cn.hutool.core.util.IdUtil;
    2. import lombok.extern.slf4j.Slf4j;
    3. import org.springframework.beans.factory.annotation.Autowired;
    4. import org.springframework.beans.factory.annotation.Value;
    5. import org.springframework.data.redis.core.StringRedisTemplate;
    6. import org.springframework.stereotype.Service;
    7. import java.util.concurrent.locks.Lock;
    8. import java.util.concurrent.locks.ReentrantLock;
    9. @Service
    10. @Slf4j
    11. public class InventoryService {
    12. @Autowired
    13. private StringRedisTemplate stringRedisTemplate;
    14. @Value("${server.port}")
    15. private String port;
    16. public String sale() {
    17. String retMessage = "";
    18. try {
    19. //1 查询库存信息
    20. String result = stringRedisTemplate.opsForValue().get("inventory001");
    21. //2 判断库存是否足够
    22. Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
    23. //3 扣减库存
    24. if(inventoryNumber > 0) {
    25. stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
    26. retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber;
    27. System.out.println(retMessage);
    28. }else{
    29. retMessage = "商品卖完了,o(╥﹏╥)o";
    30. }
    31. } finally {
    32. // 释放操作
    33. }
    34. return retMessage+"\t"+"服务端口号:"+port;
    35. }
    36. }
    1. import cn.hutool.core.util.IdUtil;
    2. import com.atguigu.redislock.service.InventoryService;
    3. import io.swagger.annotations.Api;
    4. import io.swagger.annotations.ApiOperation;
    5. import lombok.Getter;
    6. import org.springframework.beans.factory.annotation.Autowired;
    7. import org.springframework.web.bind.annotation.GetMapping;
    8. import org.springframework.web.bind.annotation.ResponseStatus;
    9. import org.springframework.web.bind.annotation.RestController;
    10. import java.util.concurrent.atomic.AtomicInteger;
    11. @RestController
    12. @Api(tags = "redis分布式锁测试")
    13. public class InventoryController {
    14. @Autowired
    15. private InventoryService inventoryService;
    16. @ApiOperation("扣减库存,一次卖一个")
    17. @GetMapping(value = "/inventory/sale")
    18. public String sale() {
    19. return inventoryService.sale();
    20. }
    21. }

    丝袜哥测试地址:http://localhost:7777/swagger-ui.html#/

    V2.0 版本

    使用 synchronized 或者 lock 进行加锁操作

    1. import cn.hutool.core.util.IdUtil;
    2. import lombok.extern.slf4j.Slf4j;
    3. import org.springframework.beans.factory.annotation.Autowired;
    4. import org.springframework.beans.factory.annotation.Value;
    5. import org.springframework.data.redis.core.StringRedisTemplate;
    6. import org.springframework.stereotype.Service;
    7. import java.util.concurrent.locks.Lock;
    8. import java.util.concurrent.locks.ReentrantLock;
    9. @Service
    10. @Slf4j
    11. public class InventoryService {
    12. @Autowired
    13. private StringRedisTemplate stringRedisTemplate;
    14. @Value("${server.port}")
    15. private String port;
    16. private Lock lock = new ReentrantLock();
    17. public String sale() {
    18. String retMessage = "";
    19. lock.lock();
    20. try {
    21. //1 查询库存信息
    22. String result = stringRedisTemplate.opsForValue().get("inventory001");
    23. //2 判断库存是否足够
    24. Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
    25. //3 扣减库存
    26. if(inventoryNumber > 0) {
    27. stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
    28. retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber;
    29. System.out.println(retMessage);
    30. }else{
    31. retMessage = "商品卖完了,o(╥﹏╥)o";
    32. }
    33. }finally {
    34. lock.unlock();
    35. }
    36. return retMessage+"\t"+"服务端口号:"+port;
    37. }
    38. }

    若在添加一个相同服务,另一个服务为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 版本

    使用递归进行重试

    1. import cn.hutool.core.util.IdUtil;
    2. import lombok.extern.slf4j.Slf4j;
    3. import org.springframework.beans.factory.annotation.Autowired;
    4. import org.springframework.beans.factory.annotation.Value;
    5. import org.springframework.data.redis.core.StringRedisTemplate;
    6. import org.springframework.stereotype.Service;
    7. import java.util.concurrent.TimeUnit;
    8. import java.util.concurrent.locks.Lock;
    9. import java.util.concurrent.locks.ReentrantLock;
    10. @Service
    11. @Slf4j
    12. public class InventoryService {
    13. @Autowired
    14. private StringRedisTemplate stringRedisTemplate;
    15. @Value("${server.port}")
    16. private String port;
    17. private Lock lock = new ReentrantLock();
    18. public String sale() {
    19. String retMessage = "";
    20. String key = "zzyyRedisLock";
    21. String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();
    22. Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue);
    23. if(!flag){
    24. //暂停20毫秒后递归调用
    25. try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
    26. sale();
    27. }else{
    28. try{
    29. //1 查询库存信息
    30. String result = stringRedisTemplate.opsForValue().get("inventory001");
    31. //2 判断库存是否足够
    32. Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
    33. //3 扣减库存
    34. if(inventoryNumber > 0) {
    35. stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
    36. retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber;
    37. System.out.println(retMessage);
    38. }else{
    39. retMessage = "商品卖完了,o(╥﹏╥)o";
    40. }
    41. }finally {
    42. stringRedisTemplate.delete(key);
    43. }
    44. }
    45. return retMessage+"\t"+"服务端口号:"+port;
    46. }
    47. }

    结果:

    1、手工测试OK,使用Jmeter压测5000也OK

    2、递归是一种思想没错,但是容号导致StackOverflowError,不太推荐,需进一步完善

    V3.2 版本

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

    1. import cn.hutool.core.util.IdUtil;
    2. import lombok.extern.slf4j.Slf4j;
    3. import org.springframework.beans.factory.annotation.Autowired;
    4. import org.springframework.beans.factory.annotation.Value;
    5. import org.springframework.data.redis.core.StringRedisTemplate;
    6. import org.springframework.stereotype.Service;
    7. import java.util.concurrent.TimeUnit;
    8. import java.util.concurrent.locks.Lock;
    9. import java.util.concurrent.locks.ReentrantLock;
    10. @Service
    11. @Slf4j
    12. public class InventoryService {
    13. @Autowired
    14. private StringRedisTemplate stringRedisTemplate;
    15. @Value("${server.port}")
    16. private String port;
    17. private Lock lock = new ReentrantLock();
    18. public String sale() {
    19. String retMessage = "";
    20. String key = "zzyyRedisLock";
    21. String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();
    22. while(!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue)){
    23. //暂停20毫秒,类似CAS自旋
    24. try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
    25. }
    26. try {
    27. //1 查询库存信息
    28. String result = stringRedisTemplate.opsForValue().get("inventory001");
    29. //2 判断库存是否足够
    30. Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
    31. //3 扣减库存
    32. if(inventoryNumber > 0) {
    33. stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
    34. retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber;
    35. System.out.println(retMessage);
    36. }else{
    37. retMessage = "商品卖完了,o(╥﹏╥)o";
    38. }
    39. }finally {
    40. stringRedisTemplate.delete(key);
    41. }
    42. return retMessage+"\t"+"服务端口号:"+port;
    43. }
    44. }

    问题:部署了微服务的Java程序机器挂了,代码层面根本没有走到finallyi这块,没办法保证解锁(无过期时间该key一直存在),这个kev没有被删除,需要加入一个过期时间限定key。

    V4.1 版本

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

    结论:虽然加了过期时间,但是设置key+过期时间分开了,不具备原子性。

    V4.2 版本

    1. import cn.hutool.core.util.IdUtil;
    2. import lombok.extern.slf4j.Slf4j;
    3. import org.springframework.beans.factory.annotation.Autowired;
    4. import org.springframework.beans.factory.annotation.Value;
    5. import org.springframework.data.redis.core.StringRedisTemplate;
    6. import org.springframework.stereotype.Service;
    7. import java.util.concurrent.TimeUnit;
    8. import java.util.concurrent.locks.Lock;
    9. import java.util.concurrent.locks.ReentrantLock;
    10. @Service
    11. @Slf4j
    12. public class InventoryService {
    13. @Autowired
    14. private StringRedisTemplate stringRedisTemplate;
    15. @Value("${server.port}")
    16. private String port;
    17. private Lock lock = new ReentrantLock();
    18. public String sale() {
    19. String retMessage = "";
    20. String key = "zzyyRedisLock";
    21. String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();
    22. while(!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue,30L,TimeUnit.SECONDS)){
    23. //暂停毫秒
    24. try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
    25. }
    26. try {
    27. //1 查询库存信息
    28. String result = stringRedisTemplate.opsForValue().get("inventory001");
    29. //2 判断库存是否足够
    30. Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
    31. //3 扣减库存
    32. if(inventoryNumber > 0) {
    33. stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
    34. retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber;
    35. System.out.println(retMessage);
    36. }else{
    37. retMessage = "商品卖完了,o(╥﹏╥)o";
    38. }
    39. }finally {
    40. stringRedisTemplate.delete(key);
    41. }
    42. return retMessage+"\t"+"服务端口号:"+port;
    43. }
    44. }

    使用Jmeter压测OK

    结论加锁和过期时间设置必须同一行,保证原子性。

    V5.0 版本

    4.2版本出现的问题:实际业务处理时间如果超过了默认设晋key的过期时间怎么办??张冠李戴,别除了别人的锁(只能自己除自己的,不许动别人的)

    改进:

    1. import cn.hutool.core.util.IdUtil;
    2. import lombok.extern.slf4j.Slf4j;
    3. import org.springframework.beans.factory.annotation.Autowired;
    4. import org.springframework.beans.factory.annotation.Value;
    5. import org.springframework.data.redis.core.StringRedisTemplate;
    6. import org.springframework.stereotype.Service;
    7. import java.util.concurrent.TimeUnit;
    8. import java.util.concurrent.locks.Lock;
    9. import java.util.concurrent.locks.ReentrantLock;
    10. @Service
    11. @Slf4j
    12. public class InventoryService {
    13. @Autowired
    14. private StringRedisTemplate stringRedisTemplate;
    15. @Value("${server.port}")
    16. private String port;
    17. private Lock lock = new ReentrantLock();
    18. public String sale() {
    19. String retMessage = "";
    20. String key = "zzyyRedisLock";
    21. String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();
    22. while(!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue,30L,TimeUnit.SECONDS)) {
    23. //暂停毫秒
    24. try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
    25. }
    26. try {
    27. //1 查询库存信息
    28. String result = stringRedisTemplate.opsForValue().get("inventory001");
    29. //2 判断库存是否足够
    30. Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
    31. //3 扣减库存
    32. if(inventoryNumber > 0) {
    33. stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
    34. retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber+"\t"+uuidValue;
    35. System.out.println(retMessage);
    36. }else{
    37. retMessage = "商品卖完了,o(╥﹏╥)o";
    38. }
    39. }finally {
    40. // v5.0判断加锁与解锁是不是同一个客户端,同一个才行,自己只能删除自己的锁,不误删他人的
    41. if(stringRedisTemplate.opsForValue().get(key).equalsIgnoreCase(uuidValue)){
    42. stringRedisTemplate.delete(key);
    43. }
    44. }
    45. return retMessage+"\t"+"服务端口号:"+port;
    46. }
    47. }

    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

    解决方式

    1. import cn.hutool.core.util.IdUtil;
    2. import lombok.extern.slf4j.Slf4j;
    3. import org.springframework.beans.factory.annotation.Autowired;
    4. import org.springframework.beans.factory.annotation.Value;
    5. import org.springframework.data.redis.core.StringRedisTemplate;
    6. import org.springframework.data.redis.core.script.DefaultRedisScript;
    7. import org.springframework.stereotype.Service;
    8. import java.util.Arrays;
    9. import java.util.concurrent.TimeUnit;
    10. import java.util.concurrent.locks.Lock;
    11. import java.util.concurrent.locks.ReentrantLock;
    12. @Service
    13. @Slf4j
    14. public class InventoryService {
    15. @Autowired
    16. private StringRedisTemplate stringRedisTemplate;
    17. @Value("${server.port}")
    18. private String port;
    19. private Lock lock = new ReentrantLock();
    20. public String sale() {
    21. String retMessage = "";
    22. String key = "zzyyRedisLock";
    23. String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();
    24. while(!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue,30L,TimeUnit.SECONDS)) {
    25. //暂停毫秒
    26. try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
    27. }
    28. try {
    29. //1 查询库存信息
    30. String result = stringRedisTemplate.opsForValue().get("inventory001");
    31. //2 判断库存是否足够
    32. Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
    33. //3 扣减库存
    34. if(inventoryNumber > 0) {
    35. stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
    36. retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber+"\t"+uuidValue;
    37. System.out.println(retMessage);
    38. }else{
    39. retMessage = "商品卖完了,o(╥﹏╥)o";
    40. }
    41. }finally {
    42. //V6.0 将判断+删除自己的合并为lua脚本保证原子性
    43. String luaScript =
    44. "if (redis.call('get',KEYS[1]) == ARGV[1]) then " +
    45. "return redis.call('del',KEYS[1]) " +
    46. "else " +
    47. "return 0 " +
    48. "end";
    49. stringRedisTemplate.execute(new DefaultRedisScript<>(luaScript, Boolean.class), Arrays.asList(key), uuidValue);
    50. }
    51. return retMessage+"\t"+"服务端口号:"+port;
    52. }
    53. }

    BUG 说明

    V7.0 版本(可重入锁)

    6.0版本:while判断并自旋重试获取锁+setx含自然过期时间+ua脚本官网除锐命令。那么问题来了:如何兼顾锁的可重入性问题?

    一个靠谱分布式锁需要具备的条件和刚需:独占性、高可用、防死锁、不乱抢、重入性

    可重入锁

    说明:可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。如果是1个有 synchronized 修饰的递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚。所以Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。

    “可重入锁”这四个字分开来解释:

    可:可以。
    重:再次。
    入:进入。
    锁:同步锁。

    进入什么:进入同步域(即同步代码快/方法或显式锁锁定的代码)

    一句话:一个线程中的多个流程可以获取同一把锁,持有这把同步锁句以再次进入。自己可以获取自己的内部锁。

    可重入锁种类:

    第一种:隐式锁(即synchronized关键字使用的锁)默认是可重入锁。

    指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。

    简单的来说就是:在一个synchronized修饰的方法或代码块的内部调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁的。

    与可重入锁相反,不可重入锁不可递归调用,递归调用就发生死锁。

    同步块:

    1. public class ReEntryLockDemo {
    2. public static void main(String[] args) {
    3. final Object objectLockA = new Object();
    4. new Thread(() -> {
    5. synchronized (objectLockA) {
    6. System.out.println("-----外层调用");
    7. synchronized (objectLockA) {
    8. System.out.println("-----中层调用");
    9. synchronized (objectLockA) {
    10. System.out.println("-----内层调用");
    11. }
    12. }
    13. }
    14. },"a").start();
    15. }
    16. }

    同步方法:

    1. /**
    2. * 在一个Synchronized修饰的方法或代码块的内部调用本类的其他Synchronized修饰的方法或代码块时,是永远可以得到锁的
    3. */
    4. public class ReEntryLockDemo {
    5. public synchronized void m1() {
    6. System.out.println("-----m1");
    7. m2();
    8. }
    9. public synchronized void m2() {
    10. System.out.println("-----m2");
    11. m3();
    12. }
    13. public synchronized void m3() {
    14. System.out.println("-----m3");
    15. }
    16. public static void main(String[] args) {
    17. ReEntryLockDemo reEntryLockDemo = new ReEntryLockDemo();
    18. reEntryLockDemo.m1();
    19. }
    20. }

    实现原理:

            每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么 Java 虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。

    第二种:显式锁(即Lock)也有ReentrantLock这样的可重入锁。

    1. import java.util.concurrent.locks.Lock;
    2. import java.util.concurrent.locks.ReentrantLock;
    3. /**
    4. * 在一个Synchronized修饰的方法或代码块的内部调用本类的其他Synchronized修饰的方法或代码块时,是永远可以得到锁的
    5. */
    6. public class ReEntryLockDemo {
    7. static Lock lock = new ReentrantLock();
    8. public static void main(String[] args) {
    9. new Thread(() -> {
    10. lock.lock();
    11. try {
    12. System.out.println("----外层调用lock");
    13. lock.lock();
    14. try {
    15. System.out.println("----内层调用lock");
    16. }finally {
    17. // 这里故意注释,实现加锁次数和释放次数不一样
    18. // 由于加锁次数和释放次数不一样,第二个线程始终无法获取到锁,导致一直在等待。
    19. lock.unlock(); // 正常情况,加锁几次就要解锁几次
    20. }
    21. }finally {
    22. lock.unlock();
    23. }
    24. },"a").start();
    25. new Thread(() -> {
    26. lock.lock();
    27. try {
    28. System.out.println("b thread----外层调用lock");
    29. }finally {
    30. lock.unlock();
    31. }
    32. },"b").start();
    33. }
    34. }

    备注:一般而言,你lock了几次就要unlock几次。

    Redis 可重入锁数据类型

    使用>结构:Map>

    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程序

    原程序:(初始无锁版)

    1. import lombok.extern.slf4j.Slf4j;
    2. import org.springframework.beans.factory.annotation.Autowired;
    3. import org.springframework.beans.factory.annotation.Value;
    4. import org.springframework.data.redis.core.StringRedisTemplate;
    5. import org.springframework.stereotype.Service;
    6. @Service
    7. @Slf4j
    8. public class InventoryService {
    9. @Autowired
    10. private StringRedisTemplate stringRedisTemplate;
    11. @Value("${server.port}")
    12. private String port;
    13. public String sale() {
    14. String retMessage = "";
    15. //1 查询库存信息
    16. String result = stringRedisTemplate.opsForValue().get("inventory001");
    17. //2 判断库存是否足够
    18. Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
    19. //3 扣减库存
    20. if(inventoryNumber > 0) {
    21. stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
    22. retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber+"\t";
    23. System.out.println(retMessage);
    24. }else{
    25. retMessage = "商品卖完了,o(╥﹏╥)o";
    26. }
    27. return retMessage+"\t"+"服务端口号:"+port;
    28. }
    29. }

    使用工厂设计模式,新建RedisDistributedLock类并实现UUC里面的Lock接口,满足JUC里面AQS对Lock锁的接口规范定义来进行实现落地代码。

    1. import org.springframework.beans.factory.annotation.Autowired;
    2. import org.springframework.data.redis.core.StringRedisTemplate;
    3. import org.springframework.stereotype.Component;
    4. import java.util.concurrent.locks.Lock;
    5. @Component
    6. public class DistributedLockFactory {
    7. @Autowired
    8. private StringRedisTemplate stringRedisTemplate;
    9. private String lockName;
    10. public Lock getDistributedLock(String lockType) {
    11. if(lockType == null) return null;
    12. if(lockType.equalsIgnoreCase("REDIS")){
    13. lockName = "zzyyRedisLock";
    14. return new RedisDistributedLock(stringRedisTemplate,lockName);
    15. } else if(lockType.equalsIgnoreCase("ZOOKEEPER")){
    16. //TODO zookeeper版本的分布式锁实现
    17. return new ZookeeperDistributedLock();
    18. } else if(lockType.equalsIgnoreCase("MYSQL")){
    19. //TODO mysql版本的分布式锁实现
    20. return null;
    21. }
    22. return null;
    23. }
    24. }
    1. import cn.hutool.core.util.IdUtil;
    2. import org.springframework.beans.factory.annotation.Autowired;
    3. import org.springframework.data.redis.core.StringRedisTemplate;
    4. import org.springframework.data.redis.core.script.DefaultRedisScript;
    5. import org.springframework.data.redis.support.collections.DefaultRedisList;
    6. import org.springframework.stereotype.Component;
    7. import java.util.Arrays;
    8. import java.util.concurrent.TimeUnit;
    9. import java.util.concurrent.locks.Condition;
    10. import java.util.concurrent.locks.Lock;
    11. //@Component 引入DistributedLockFactory工厂模式,从工厂获得而不再从spring拿到
    12. public class RedisDistributedLock implements Lock {
    13. private StringRedisTemplate stringRedisTemplate;
    14. private String lockName;//KEYS[1]
    15. private String uuidValue;//ARGV[1]
    16. private long expireTime;//ARGV[2]
    17. public RedisDistributedLock(StringRedisTemplate stringRedisTemplate, String lockName) {
    18. this.stringRedisTemplate = stringRedisTemplate;
    19. this.lockName = lockName;
    20. this.uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();//UUID:ThreadID
    21. this.expireTime = 30L;
    22. }
    23. @Override
    24. public void lock(){
    25. tryLock();
    26. }
    27. @Override
    28. public boolean tryLock(){
    29. try {tryLock(-1L,TimeUnit.SECONDS);} catch (InterruptedException e) {e.printStackTrace();}
    30. return false;
    31. }
    32. /**
    33. * 干活的,实现加锁功能,实现这一个干活的就OK,全盘通用
    34. * @param time
    35. * @param unit
    36. * @return
    37. * @throws InterruptedException
    38. */
    39. @Override
    40. public boolean tryLock(long time, TimeUnit unit) throws InterruptedException{
    41. if(time != -1L){
    42. this.expireTime = unit.toSeconds(time);
    43. }
    44. String script =
    45. "if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then " +
    46. "redis.call('hincrby',KEYS[1],ARGV[1],1) " +
    47. "redis.call('expire',KEYS[1],ARGV[2]) " +
    48. "return 1 " +
    49. "else " +
    50. "return 0 " +
    51. "end";
    52. System.out.println("script: "+script);
    53. System.out.println("lockName: "+lockName);
    54. System.out.println("uuidValue: "+uuidValue);
    55. System.out.println("expireTime: "+expireTime);
    56. while (!stringRedisTemplate.execute(new DefaultRedisScript<>(script,Boolean.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime))) {
    57. TimeUnit.MILLISECONDS.sleep(50);
    58. }
    59. return true;
    60. }
    61. /**
    62. *干活的,实现解锁功能
    63. */
    64. @Override
    65. public void unlock() {
    66. String script =
    67. "if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then " +
    68. " return nil " +
    69. "elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then " +
    70. " return redis.call('del',KEYS[1]) " +
    71. "else " +
    72. " return 0 " +
    73. "end";
    74. // nil = false 1 = true 0 = false
    75. System.out.println("lockName: "+lockName);
    76. System.out.println("uuidValue: "+uuidValue);
    77. System.out.println("expireTime: "+expireTime);
    78. Long flag = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime));
    79. if(flag == null) {
    80. throw new RuntimeException("This lock doesn't EXIST");
    81. }
    82. }
    83. //===下面的redis分布式锁暂时用不到=======================================
    84. //===下面的redis分布式锁暂时用不到=======================================
    85. //===下面的redis分布式锁暂时用不到=======================================
    86. @Override
    87. public void lockInterruptibly() throws InterruptedException {
    88. }
    89. @Override
    90. public Condition newCondition() {
    91. return null;
    92. }
    93. }

    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 : 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 : 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 == nullreturn 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 : 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 : 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、锁的自动续期

    总结实现过程

    1、按照juc里面java.util.concurrent.locks.Lock接口规范编写

    2、lock()加锁实现逻辑

    a、加锁的Lua脚本,通过redis里面的hash数据模型,加锁和可重入性都要保证

    b、加锁不成,需要while进行重试并自旋

    c、自动续期,加个钟

    3、 unlock()解锁实现逻辑

    a、考虑可重入性的递减,加锁几次就要减锁几次

    b、最后到零了,直接del删除

    注:将Key键删除。但也不能乱删,不的说客户端1的请求将客户端2的锁给删除掉,只能自己删除自己的锁

  • 相关阅读:
    【数据结构】双向链表(C语言)
    算法笔记-第十章-图的遍历(未处理完-11.22日)
    uni-app 之 vue位置怎样设置
    内网穿透实现Windows远程桌面访问Ubuntu,简单高效的远程桌面解决方案
    Sentinel —实时监控
    docker network 删除失败 has active endpoint
    网游服务器怎么选择
    C++ 智能指针总结
    DX 的 HLSL 和 GL 的 GLSL的 矩阵构建的行列区别
    训练营第二十九天贪心(简单题目)
  • 原文地址:https://blog.csdn.net/qq_36942720/article/details/132722972