15、分布式锁简介

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

分布式锁的核心思想就是让大家都使用同一把锁,只要大家使用的是同一把锁,那么我们就能锁住线程,不让线程进行,让程序串行执行,这就是分布式锁的核心思路
在这里插入图片描述
那么分布式锁他应该满足一些条件:

可见性:多个线程都能看到相同的结果,注意:这个地方说的可见性并不是并发编程中指的内存可见 性,只是说多个进程之间都能感知到变化的意思
互斥:互斥是分布式锁的最基本的条件,使得程序串行执行
高可用:程序不易崩溃,时时刻刻都保证较高的可用性
高性能:由于加锁本身就让性能降低,所有对于分布式锁本身需要他就较高的加锁性能和释放锁性能
安全性:安全也是程序中必不可少的一环

常见的分布式锁有三种

Mysql:mysql本身就带有锁机制,但是由于mysql性能本身一般,所以采用分布式锁的情况下,其实使用mysql作为分布式锁比较少见
Redis:redis作为分布式锁是非常常见的一种使用方式,现在企业级开发中基本都使用redis或者zookeeper作为分布式锁,利用setnx这个方法,如果插入key成功,则表示获得到了锁,如果有人插入成功,其他人插入失败则表示无法获得到锁,利用这套逻辑来实现分布式锁
Zookeeper:zookeeper也是企业级开发中较好的一个实现分布式锁的方案
在这里插入图片描述
在这里插入图片描述
非原子操作(setnx + expire)

一说到实现 Redis 的分布式锁,很多小伙伴马上就会想到 setnx+ expire 命令。也就是说,先用 setnx 来抢锁,如果抢到之后,再用 expire 给锁设置一个 过期时间。

伪代码如下:

if(jedis.setnx(lock_key,lock_value) == 1{ //加锁

jedis.expire(lock_key,timeout); //设置过期时间

doBusiness //业务逻辑处理

}

这块代码是 有坑的,因为 setnx 和 expire 两个命令是分开写的,并不是原子操作!如果刚要执行完 setnx 加锁,正要执行 expire 设置过期时间时,进程 crash 或者要重启维护了,那么这个锁就“ 长生不老”了,别的线程永远获取不到锁啦。

Base案例(SpringBoot+Redis)
使用场景:多个服务间保证同一时刻同一时间段内同一个用户只能有一个请求(防止关键业务出现并发攻击)

建module
redis_distributed_lock2redis_distributed_lock3
改POM

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <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>
        <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>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.2.3</version>
        </dependency>
    </dependencies>

写YML

server.port=7777
spring.application.name=redis_distributed_lock2
spring.mvc.pathmatch.matching-strategy=ant_path_matcher
# ========================================Redis单机############################
spring.redis.database=0
spring.redis.host=192.168.10.101
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

业务类


@Service
@Slf4j
public class InventoryService {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

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

    private Lock lock = new ReentrantLock();

    public String sale() {
        String message = "";
        lock.lock();
        try {
            //查询库存信息
            String result = stringRedisTemplate.opsForValue().get("inventory001");
            //判断库存是否足够
            Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
            //扣减库存,每次扣减一个
            if (inventoryNumber > 0) {
                stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
                message = "成功卖出商品,库存剩余: " + inventoryNumber;
                log.info(message + "\t" + "服务端口号:" + port);
            } else {
                message = "商品卖完了,(;′⌒`)";
            }
        } finally {
            lock.unlock();
        }
        return message + "\t" + "服务端口号:" + port;
    }
}

@Api(tags = "redis分布式测试")
@RestController
@Slf4j
public class InventoryController {
    @Autowired
    private InventoryService inventoryService;


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

手写分布式锁思路分析

初始化版本简单添加
将7777的业务逻辑代码原样拷贝到8888

nginx分布式微服务架构
v2.0版本代码分布式部署后,单机锁还是出现超卖现象,需要分布式锁

Nginx配置负载均衡
在这里插入图片描述
● 配置地址:/usr/local/nginx/conf (我的在windows系统下)
● 修改配置文件,nginx.conf新增反向代理和负载均衡配置
○
v2.0版本代码修改+启动
Nginx访问,可以看到效果,一边一个,默认轮询。
在这里插入图片描述
手工验证ok,开始高并发模拟
使用jmete进行压测
在这里插入图片描述
bug-why
在单机环境下,可以使用synchronized或lock来实现,但是在分布式系统中,因为竞争的线程可能不在同一节点上(同一个JVM中),所以需要一个让所有进程都能访问到的锁来实现(比如redis或者zookeeper来构建)
不同进程JVM层面的锁就不管用了,那么可以利用第三方的一个组件,来获取锁,未获取到锁,则阻塞当前想要运行的线程

分布式锁出现
● 跨进程+跨服务
● 解决超卖
● 防止缓存击穿
解决
Redis具有极高的性能且其命令对分布式锁支持友好,借助SET命令即可实现加锁处理
在这里插入图片描述
Redis分布式锁

递归方式,不推荐,容易


package com.atguigu.redislock.service;

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

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

/**
 * @auther zzyy
 * @create 2022-10-22 15:14
 */
@Service
@Slf4j
public class InventoryService
{
    @Autowired
    private 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;
    }
}

while代替if,相当于juc的虚假唤醒,自旋

@Service
@Slf4j
public class InventoryService {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

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


    public String sale() {
        String retMessage = "";
        String key = "wghRedisLock";
        String value = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();

        // 不用递归了,高并发下容易出错,我们用自旋替代递归方法调用;也不用if,用while来替代
        while (!stringRedisTemplate.opsForValue().setIfAbsent(key, value)) {
            // 暂停20毫秒,进行递归重试
            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程序挂了,代码层面根本没有走到finally这块,没办法保证解锁(无过期时间,改key一直存在),这个key没有被删除,需要加入一个过期时间限定key。

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

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

在这里插入图片描述
设置key+过期时间必须合并一行保证原子性

package com.atguigu.redislock.service;

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

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


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

    private Lock lock = new ReentrantLock();

    public String sale()
    {
        String retMessage = "";
        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;
    }
}
 

 

实际业务处理时间如果超过了默认设置key的过期时间,张冠李戴,就会把别人的锁删除了!!!
我们的要求是只能删除自己的,不能动别人的。
在这里插入图片描述

package com.atguigu.redislock.service;

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

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

/**
 * @auther zzyy
 * @create 2022-10-22 15:14
 */
@Service
@Slf4j
public class InventoryService
{
    @Autowired
    private 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;
    }
}
 

 

Lua保证原子性

最后的判断+delete不是原子操作,需要用Lua脚本进行修改。
Lua脚本浅谈
在这里插入图片描述

Redis调用Lua脚本通过eval命令保证代码执行的原子性,直接用return返回脚执行后的结果值

命令:eval luascript numkeys [key[key…]] [arg[arg…]]
eval “redis.call(‘set’,‘k1’,‘v1’) redis.call(‘expire’,‘k1’,‘30’) return redis.call(‘get’,‘k1’)” 0
eval “return redis.call(‘mset’,KEYS[1],ARGV[1],KEYS[2],ARGV[2])” 2 k1 k2 lua1 lua2

在这里插入图片描述
进阶判断:eval “if redis.call(‘get’,KEYS[1])==ARGV[1] then return redis.call(‘del’,KEYS[1]) else return 0 end” 1 wghRedisLock 11112222
在这里插入图片描述
在这里插入图片描述

package com.atguigu.redislock.service;

import cn.hutool.core.util.IdUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

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

/**
 * @auther zzyy
 * @create 2022-10-22 15:14
 */
@Service
@Slf4j
public class InventoryService
{
    @Autowired
    private 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;
    }
}

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
第1章 课程介绍 介绍该课程的内容、学习成果、实例,还有学习所需的前提知识。 1-1 导学-分布式事务实践 第2章 事务原则与实现 介绍了事务的四大原则,并通过实例介绍数据库实现事务的方法,以及使用JDBC实现事务的方法。 2-1 事务原则与实现:事务 2-2 事务原则与实现:SQL事务 2-3 事务原则与实现:JDBC事务(上) 2-4 事务原则与实现:JDBC事务(下) 第3章 使用Docker搭建环境 介绍了Docker的使用,通过Docker将课程环境搭建起来,方便那些不了解这些技术的同学之后的学习。 3-1 docker简介与mysql安装-1 3-2 docker简介与mysql安装-2 3-3 SpringBoot基础 第4章 Spring事务机制 介绍了Spring的事务机制、事物抽象、内部事务和外部事物,以及常用的几种事务管理的实现,包括DataSource、JPA、JMS、JTA都通过实例进行说明。还有XA以及两阶段提交,并通过实例演示了使用JTA,通过两阶段提交,实现多数据源的事务实现。... 4-1 Spring事务机制_基本接口 4-2 Spring事务机制_实现 4-3 Jpa事务实例 4-4 Jms事务原理 4-5 Jms-session事务实例 4-6 Jms-spring事务实例 4-7 外部事务与JTA 4-8 JTA单数据源事务实例 4-9 JTA多数据源事务实例 第5章 分布式系统 介绍了分布式系统的定义、实现原则和几种形式,详细介绍了微服务架构的分布式系统,并使用Spring Cloud框架演示了一个完整的微服务系统的实现过程。 5-1 CAP原则和BASE理论简介 5-2 分布式系统综述 5-3 SpringCloud微服务架构 5-4 实现registry 5-5 实现proxy 5-6 user服务 5-7 order服务 5-8 添加hystrix 5-9 使用feign 5-10 优化服务间调用 第6章 分布式事务实现,模式和技术 介绍分布式事务的定义、原则和实现原则,介绍使用Spring框架实现分布式事务的几种方式,包括使用JTA、Spring事务同步、链式事务等,并通过实战介绍其实现。除此以外还介绍了一些分布式事务相关的技术,如幂等性、全局一致性ID、分布式对象等。... 6-1 分布式事务介绍 6-2 spring分布式事务实现_使用JTA 6-3 spring分布式事务实现_不使用JTA 6-4 实例1-DB-DB 6-5 实例1-DB-DB.链式事务管理器 6-6 实例2-JPA-DB.链式事务管理器 6-7 实例3-JMS-DB.最大努力一次提交 6-8 分布式事务实现模式与技术 6-9 全局一致性ID和分布式对象_ 第7章 分布式事务实现:消息驱动模式 详细介绍3种分布式事务实现的模式中的消息驱动模式并通过完整实例演示了消息驱动模式下,实现微服务系统的分布式事务的完整过程。 7-1 分布式事务实现:消息驱动模式 7-2 消息驱动模式实例:设计 7-3 消息驱动模式实例:创建ticket服务 7-4 消息驱动模式实例:实现基本ticket功能 7-5 消息驱动模式实例:锁票1 7-6 消息驱动模式实例:锁票2 7-7 按消息流程实现业务 7-8 支付过程 7-9 票转移 7-10 错误处理:锁票失败 7-11 错误处理:扣费失败 7-12 并发时的错误处理 第8章 分布式事务实现:Event Sourcing模式 详细介绍了分布式事务实现的模式中的Event Sourcing模式,并通过完整实例演示了Event Sourcing模式下,实现微服务系统的分布式事务的完整过程。 8-1 事件溯源模式介绍 8-2 事件溯源模式与Axon框架-1 8-3 事件溯源模式与Axon框架-2 8-4 使用Axon框架的设计过程介绍 8-5 Axon框架-实例(上) 8-6 Axon框架-实例(下) 8-7 Saga模式和Axon Saga 8-8 聚合命令事件(上) 8-9 聚合命令事件(下) 8-10 实现saga 8-11 实现query 8-12 处理超时 8-13 并发测试 8-14 cloud-axon实例:分布式处理介绍 8-15 事件设计 8-16 事件与队列设计 8-17 实现User服务 8-18 实现Ticket服务 8-19 实现Order服务 8-20 实现读写分离 8-21 测试与并发 8-22 事件溯源模式与Axon框架总结 第9章 TCC模式和微服务架构的设计模式 本章介绍TCC模式,也对微服务系统的几种设计模式,以及这些模式下分布式事务的实现模式进行了介绍。 9-1 TCC模式介绍 9-2 微服务架构的设计模式 第10章 课程总

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

princeAladdin

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值