分布式锁的应用场景与分布式锁实现(一):传统锁处理并发及传统锁的问题

news2024/11/29 10:57:39

分布式锁

代码已同步至GitCode:https://gitcode.net/ruozhuliufeng/distributed-project.git

​ 在应用开发中,特别是Web工程开发,通常都是并发编程,不是多进程就是多线程。这种场景下极其容易出现线程并发性问题,此时不得不使用锁来解决问题。在多线程高并发场景下,为了保证资源的线程安全问题,jdk为我们提供了synchronized关键字和ReentrantLock可重入锁,但是它们只能提供一个工程内的线程安全。在分布式集群、微服务、云原生横行的当下,如何保证不同进程、不同服务、不同机器的线程安全问题,JDK并没有给我们提供既有的解决方案。此时,我们就必须借助于相关技术手动实现了。目前主流的实现有以下方式:

  • 基于MySQL关系型实现
  • 基于Redis非关系型数据实现
  • 基于Zookeeper/etcd实现

问题引入

从减库存说起

​ 多线程并发安全问题最典型的代表就是超卖现象。

​ 库存存在并发量较大情况下,很容易发生超卖现象,一旦发生超卖现象,就会出现多成交了订单而发不了货的情况。

场景:

​ 商品S库存余量为5时,用户A与用户B同时来购买一个商品,此时查询库存数都为5,库存充足则开始减库存:

​ 用户A: update db_stock set stock=stock-1 where id = 1

​ 用户B: update db_stock set stock=stock-1 where id = 1

​ 在并发情况下,更新后的结果可能是4,而实际的最终库存量应该是3才对。

环境准备

  • 数据库:MySQL 5.7
  • JAVA版本:1.8
  • 工程构建工具:Maven
  • 框架:SpringBoot、SpringMVC、MyBatis-Plus、SpringDataRedis
  • 开发工具:IDEA
  • 缓存服务:Redis
  • 负载均衡工具:Nginx
  • 接口与压测工具:Jmeter

创建基础数据表

  • 创建数据库表:db_stock
CREATE TABLE `db_stock` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `product_code` varchar(255) DEFAULT NULL COMMENT '商品编号',
  `stock_code` varchar(255) DEFAULT NULL COMMENT '仓库编号',
  `count` int(11) DEFAULT NULL COMMENT '库存量',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
  • 新增测试数据
INSERT INTO `distributed_lock`.`db_stock` (`id`, `product_code`, `stock_code`, `count`) VALUES (1, '1001', '001', 5000);

创建分布式锁demo工程

  • 使用IDEA新建SpringBoot项目,本次测试项目名:distributed-lock
  • 更新pom.xml文件,新增相关依赖
    <dependencies>
        <!-- Spring -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <!--springboot默认使用内置tomcat,需要手动排除然后引入undertow(各方面性能更好,更稳定) -->
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-tomcat</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-undertow</artifactId>
        </dependency>
        <!-- MySQL -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.28</version>
        </dependency>
        <!-- MyBatis Plus -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.2</version>
        </dependency>
        <!-- Redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
    </dependencies>
  • 创建application.yml文件,配置项目信息
server:
  # 端口
  port: 8001
spring:
  # 数据库
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/distributed_lock?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai
    username: root
    password: root
  # Redis配置
  redis:
    host: localhost
    database: 0
    port: 6379
  • 启动类新增Mapper包扫描
@SpringBootApplication
@MapperScan("tech.msop.distributed.lock.mapper")
public class DistributedLockApplication {
    public static void main(String[] args) {
        SpringApplication.run(DistributedLockApplication.class, args);
    }
}
  • 新增实体类:StockEntity
/**
 * 库存信息实体
 */
@Data
@TableName("db_stock")
public class StockEntity {
    /**
     * 主键ID
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;
    /**
     * 商品编号
     */
    private String productCode;
    /**
     * 仓库编号
     */
    private String stockCode;
    /**
     * 库存量
     */
    private Integer count=5000;
}
  • 新增Mapper接口:StockMapper
public interface StockMapper extends BaseMapper<StockEntity> {
}
  • 新增Service服务:StockService

    • IStockService
    public interface IStockService extends IService<StockEntity> {
    }
    
    • StockServiceImpl
    @Service
    public class StockServiceImpl extends ServiceImpl<StockMapper, StockEntity>
            implements IStockService {
    }
    
  • 新增控制器:StockController

@RequestMapping("/stock")
@RestController
@RequiredArgsConstructor
public class StockController {
    private final IStockService stockService;

}
  • 基础项目结构如下:

image-20230526141726828

简单实现减库存

  • 修改StockController
package tech.msop.distributed.lock.controller;

import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.msop.distributed.lock.service.IStockService;

/**
 * 库存 控制器
 */
@RequestMapping("/stock")
@RestController
@RequiredArgsConstructor
public class StockController {
    private final IStockService stockService;

    /**
     * 减库存
     * @return
     */
    @GetMapping("/check/lock")
    public String checkAndLock(){
        stockService.checkAndLock();
        return "验证库存并锁库存成功";
    }

    /**
     * 库存重置
     */
    @GetMapping("/reset")
    public void reset(){
        stockService.reset();
    }

}

  • 修改StockService
package tech.msop.distributed.lock.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import tech.msop.distributed.lock.constants.StockConstant;
import tech.msop.distributed.lock.entity.StockEntity;
import tech.msop.distributed.lock.mapper.StockMapper;
import tech.msop.distributed.lock.service.IStockService;

/**
 * 库存服务实现类
 */
@Service
@Slf4j
public class StockServiceImpl extends ServiceImpl<StockMapper, StockEntity>
        implements IStockService {
    private StockEntity stock = new StockEntity();
    /**
     * 减库存
     */
    @Override
    public void checkAndLock() {
        stock.setCount(stock.getCount() - 1);
        log.info("库存余量:{}",stock.getCount());
//        // 先查询库存是否充足
//        StockEntity stock = this.getById(1);
//        // 再减1个库存
//        if (stock != null && stock.getCount() >0 ){
//            stock.setCount(stock.getCount() - 1);
//            this.updateById(stock);
//        }
    }

    /**
     * 重置库存数量
     */
    @Override
    public void reset() {
        this.baseMapper.reset(StockConstant.DEFAULT_STOCK_COUNT);
    }
}

  • 修改StockMapper
package tech.msop.distributed.lock.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import tech.msop.distributed.lock.entity.StockEntity;

public interface StockMapper extends BaseMapper<StockEntity> {

    void reset(@Param("count") Integer defaultStockCount);
}

  • 修改StockMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="tech.msop.distributed.lock.mapper.StockMapper">

    <update id="reset">
        update db_stock
        set count = #{count}
    </update>
</mapper>

  • 接口调用并测试

image-20230526143845817

  • 查看控制台

image-20230526153856480

​ 使用接口一次一次调用时,每访问一次,库存量减1,没有任何问题。

简单演示超卖现象

​ 使用Jmeter压力测试工具,高并发下压测一下。恢复库存数为5000,添加线程组:并发100循环50次,即5000次请求。

image-20230526145321738

image-20230526145423703

​ 给线程组添加HTTP Request请求

image-20230526145503242

​ 添加测试接口与请求路径

image-20230526145553926

​ 选择想要的测试报表,这里选择聚和报告:

image-20230526145703422

​ 启动测试,查看压力测试报告

image-20230526154103065

  • Label 取样器别名,如果勾选Include Group Name,则会添加线程组的名称作为前缀
  • # Samples 取样器运行测试
  • Average 请求(事务)的平均响应时间
  • Median 中位数
  • 90%Line 90%用户响应时间
  • 95%Line 95%用户响应时间
  • 99%Line 99%用户响应时间
  • Min 最小响应时间
  • Max 最大响应时间
  • Error 错误率
  • Throughput 吞吐率
  • Received KB/sec 每秒收到的千字节
  • Sent KB/sec 每秒发送的千字节

​ 测试结果:请求总数5000次,平均请求时间54ms,中位数(50%)请求在26ms内完成的,错误率0%,每秒钟平均吞吐率1396.3次。

​ 查看数据库剩余库存数:461

image-20230526154206692

​ 此时如果还有人来下单,就会出现超卖现象(别人购买成功,而无货可发)。

传统锁处理

JVM本地锁处理

使用JVM锁:synchronized关键字
package tech.msop.distributed.lock.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import tech.msop.distributed.lock.constants.StockConstant;
import tech.msop.distributed.lock.entity.StockEntity;
import tech.msop.distributed.lock.mapper.StockMapper;
import tech.msop.distributed.lock.service.IStockService;

/**
 * 库存服务实现类
 */
@Service
@Slf4j
public class StockServiceImpl extends ServiceImpl<StockMapper, StockEntity>
        implements IStockService {
    private StockEntity stock = new StockEntity();
    /**
     * 减库存
     */
    @Override
    public synchronized void checkAndLock() {
        stock.setCount(stock.getCount() - 1);
        log.info("库存余量:{}",stock.getCount());
    }
}

​ Jmeter压测测试报告:

image-20230526154726384

​ 库存余量:0

image-20230526154740075

使用JVM锁:ReetrantLock
package tech.msop.distributed.lock.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import tech.msop.distributed.lock.constants.StockConstant;
import tech.msop.distributed.lock.entity.StockEntity;
import tech.msop.distributed.lock.mapper.StockMapper;
import tech.msop.distributed.lock.service.IStockService;

import java.util.concurrent.locks.ReentrantLock;

/**
 * 库存服务实现类
 */
@Service
@Slf4j
public class StockServiceImpl extends ServiceImpl<StockMapper, StockEntity>
        implements IStockService {
    private StockEntity stock = new StockEntity();
    private ReentrantLock lock = new ReentrantLock();
    /**
     * 减库存
     */
    @Override
    public synchronized void checkAndLock() {
        lock.lock();
        try{
            stock.setCount(stock.getCount() - 1);
            log.info("库存余量:{}",stock.getCount());
        }finally {
            lock.unlock();
        }
    }
}

​ Jmeter压测测试报告:

image-20230526155231872

​ 库存余量:0

image-20230526155250238

原理

​ 添加了synchronized关键字后,StockService就具备了对象锁,由于添加了独占的排他锁,同一时刻只有一个请求能够获取到锁,并减库存。此时,所有请求只会one-by-one执行下去,也就不会发生超卖现象。

image-20230526155531198

JVM本地锁失效场景之:多例模式

​ Service添加多例模式注解,并进行压力测试

package tech.msop.distributed.lock.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Service;
import tech.msop.distributed.lock.constants.StockConstant;
import tech.msop.distributed.lock.entity.StockEntity;
import tech.msop.distributed.lock.mapper.StockMapper;
import tech.msop.distributed.lock.service.IStockService;

import java.util.concurrent.locks.ReentrantLock;

/**
 * 库存服务实现类 <br/>
 * 注意:@Scope的proxyMode,若为Spring原生,使用的是JDK代理,proxyMode应为INTERFACES,<br/>
 * SpringBoot 2.x起,使用的是CGlib代理,proxyMode为TARGET_CLASS
 */
@Service
@Slf4j
@Scope(value = "prototype",proxyMode = ScopedProxyMode.TARGET_CLASS)
public class StockServiceImpl extends ServiceImpl<StockMapper, StockEntity>
        implements IStockService {
    /**
     * 减库存
     */
    @Override
    public synchronized void checkAndLock() {
        try {
            // 先查询库存是否充足
            StockEntity stock = this.getOne(new QueryWrapper<StockEntity>().eq("product_code", "1001"));
            // 再减1个库存
            if (stock != null && stock.getCount() > 0) {
                stock.setCount(stock.getCount() - 1);
                this.updateById(stock);
            }
        } finally {
        }
    }

    /**
     * 重置库存数量
     */
    @Override
    public void reset() {
        this.baseMapper.reset(StockConstant.DEFAULT_STOCK_COUNT);
    }
}

​ 查看数据库余量:4846

image-20230526161327499

​ JVM本地锁已失效

JVM本地锁失效场景之:事务

更新库存余量为5000

​ 请求方法添加事务注解,并进行压力测试

package tech.msop.distributed.lock.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import tech.msop.distributed.lock.constants.StockConstant;
import tech.msop.distributed.lock.entity.StockEntity;
import tech.msop.distributed.lock.mapper.StockMapper;
import tech.msop.distributed.lock.service.IStockService;

import java.util.concurrent.locks.ReentrantLock;

/**
 * 库存服务实现类
 */
@Service
@Slf4j
public class StockServiceImpl extends ServiceImpl<StockMapper, StockEntity>
        implements IStockService {
    /**
     * 减库存
     * 添加事务注解
     */
    @Override
    @Transactional
    public synchronized void checkAndLock() {
        try {
            // 先查询库存是否充足
            StockEntity stock = this.getOne(new QueryWrapper<StockEntity>().eq("product_code", "1001"));
            // 再减1个库存
            if (stock != null && stock.getCount() > 0) {
                stock.setCount(stock.getCount() - 1);
                this.updateById(stock);
            }
        } finally {
        }
    }

    /**
     * 重置库存数量
     */
    @Override
    public void reset() {
        this.baseMapper.reset(StockConstant.DEFAULT_STOCK_COUNT);
    }
}

​ 查看数据库库存余量:14

image-20230526161909083

​ JVM本地锁已失效

JVM本地锁失效场景之:集群部署

修改库存余量为5000

​ 复制启动类,并命名为DistributedLockApplication2,修改启动类的端口号为8002

image-20230526162704582

​ 启动复制的服务:

image-20230526162731725

​ 编辑Nginx的配置文件nginx.conf文件,实现负载均衡

worker_processes  1;

events {
    worker_connections  1024;
}


http {
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  D:/Program/Nginx/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    #gzip  on;
	upstream distributedLock{
		server localhost:8001;
		server localhost:8002;
	}
	server{
		listen  80;
		server_name localhost;
		location / {
			proxy_pass http://distributedLock;
		}
	}
    include D:/Program/Nginx/conf/conf.d/*.conf;

}

​ 启动Nginx,修改Jmeter的HTTP请求,端口修改为80,并再次进行压力测试,查看数据库余量:2012

image-20230526163601519

​ JVM本地锁机制已失效

单SQL语句处理

在更新数量时进行判断

可以解决JVM本地锁失效的场景

​ 更新服务代码:

package tech.msop.distributed.lock.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import tech.msop.distributed.lock.constants.StockConstant;
import tech.msop.distributed.lock.entity.StockEntity;
import tech.msop.distributed.lock.mapper.StockMapper;
import tech.msop.distributed.lock.service.IStockService;

import java.util.concurrent.locks.ReentrantLock;

/**
 * 库存服务实现类 <br/>
 * 注意:@Scope的proxyMode,若为Spring原生,使用的是JDK代理,proxyMode应为INTERFACES,<br/>
 * SpringBoot 2.x起,使用的是CGlib代理,proxyMode为TARGET_CLASS
 */
@Service
@Slf4j
public class StockServiceImpl extends ServiceImpl<StockMapper, StockEntity>
        implements IStockService {
    /**
     * 减库存
     */
    @Override
    public void checkAndLock() {
        try {
            // 1.先查询库存是否充足
//            StockEntity stock = this.getOne(new QueryWrapper<StockEntity>().eq("product_code", "1001"));
//            // 2.判断库存余量
//            if (stock != null && stock.getCount() > 0) {
//                stock.setCount(stock.getCount() - 1);
//                // 3.更新到数据库
//                this.updateById(stock);
//            }

            // update insert delete写操作本身就会加锁
            // 使用一条SQL语句完成减库存操作
            // update db_stock set count = count - 1 where product_code = '1001' and count >=1
            this.baseMapper.updateStock(1,"1001");
        } finally {
        }
    }

    /**
     * 重置库存数量
     */
    @Override
    public void reset() {
        this.baseMapper.reset(StockConstant.DEFAULT_STOCK_COUNT);
    }
}

​ 更新Mapper代码:

package tech.msop.distributed.lock.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
import tech.msop.distributed.lock.entity.StockEntity;

public interface StockMapper extends BaseMapper<StockEntity> {

    void reset(@Param("count") Integer defaultStockCount);
    @Update("update db_stock set count = count - #{count} where product_code = #{productCode} and count >= #{count} ")
    void updateStock(@Param("count") int count,@Param("productCode") String productCode);
}

​ 进行压力测试,并查看数据库余量:0

image-20230526164754771

存在的问题
  • 锁范围的问题
  • 同一个商品可能有多条库存记录
  • 无法记录库存变化前后的状态

MySQL悲观锁

select … for update

在MySQL的InnoDB中,预设的Transaction isolation level为REPEATABLE READ(可重读)

​ 在SELECT的读取锁定主要分为两种方式:

  • SELECT … LOCK IN SHARE MODE (共享锁)
  • SELECT … FOR UPDATE (悲观锁)

​ 这两种方式在事务(Transaction)进行当中SELECT到同一个数据库时,都必须等待其他事务数据被提交(Commit)后才会执行。

​ 而主要的不同在于LOCK IN SHARE MODE在有一方事务要UPDATE同一个表单时很容易造成死锁。

​ 简单来说,如果SELECT后若要UPDATE同一个表单,最好使用 SELECT …. FOR UPDATE

代码实现

​ 新增数据库数据:

image-20230526165048260

​ 修改服务类:

package tech.msop.distributed.lock.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import tech.msop.distributed.lock.constants.StockConstant;
import tech.msop.distributed.lock.entity.StockEntity;
import tech.msop.distributed.lock.mapper.StockMapper;
import tech.msop.distributed.lock.service.IStockService;

import java.util.List;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 库存服务实现类 <br/>
 * 注意:@Scope的proxyMode,若为Spring原生,使用的是JDK代理,proxyMode应为INTERFACES,<br/>
 * SpringBoot 2.x起,使用的是CGlib代理,proxyMode为TARGET_CLASS
 */
@Service
@Slf4j
public class StockServiceImpl extends ServiceImpl<StockMapper, StockEntity>
        implements IStockService {
    /**
     * 减库存
     */
    @Override
    @Transactional
    public void checkAndLock() {
        // 1. 查询库存信息并锁定库存信息
        List<StockEntity> list = this.baseMapper.queryStock("1001");
        //      这里取第一个库存
        if (CollectionUtils.isEmpty(list)) {
            return;
        }
        StockEntity stock = list.get(0);
        // 2. 判断库存是否充足
        if (stock != null && stock.getCount() > 0) {
            stock.setCount(stock.getCount() - 1);
            // 3.更新到数据库
            this.updateById(stock);
        }
    }


    public void checkAndLock2() {
        try {
            // 1.先查询库存是否充足
//            StockEntity stock = this.getOne(new QueryWrapper<StockEntity>().eq("product_code", "1001"));
//            // 2.判断库存余量
//            if (stock != null && stock.getCount() > 0) {
//                stock.setCount(stock.getCount() - 1);
//                // 3.更新到数据库
//                this.updateById(stock);
//            }

            // update insert delete写操作本身就会加锁
            // 使用一条SQL语句完成减库存操作
            // update db_stock set count = count - 1 where product_code = '1001' and count >=1
            this.baseMapper.updateStock(1, "1001");
        } finally {
        }
    }

    /**
     * 重置库存数量
     */
    @Override
    public void reset() {
        this.baseMapper.reset(StockConstant.DEFAULT_STOCK_COUNT);
    }
}

​ 修改Mapper文件:

package tech.msop.distributed.lock.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import tech.msop.distributed.lock.entity.StockEntity;

import java.util.List;

public interface StockMapper extends BaseMapper<StockEntity> {

    void reset(@Param("count") Integer defaultStockCount);
    @Update("update db_stock set count = count - #{count} where product_code = #{productCode} and count >= #{count} ")
    void updateStock(@Param("count") int count,@Param("productCode") String productCode);

    @Select("select * from db_stock where product_code = #{productCode} for update")
    List<StockEntity> queryStock(@Param("productCode") String productCode);
}

​ 进行压力测试并查询数据库余量:0

image-20230526171152482

MySQL悲观锁中使用行级锁
  • 锁的查询或者更新条件必须是索引字段
  • 查询或者更新条件必须是具体值(如=、in,但like、!=条件均不可以,悲观锁仍是表级锁)
优缺点
  • 优点:
    • 解决同一个商品有多条库存记录同时更新的问题
    • 可以记录库存变化前后的状态
  • 缺点:
    • 性能问题
    • 死锁问题:对多条数据加锁时,加锁顺序要一致
    • 库存操作要统一:select … for update 普通的select

MySQL乐观锁

借助时间戳/version版本号/CAS机制实现

  • CAS:Compare And Swap(Set),比较并交换
    • 变量K 旧值A 新值B
    • 如用户更新密码,输入旧密码 A 与新密码 B,根据用户名 K 判断用户密码与旧密码是否一致,若一致,更新为新密码,否则放弃本次修改
  • 每次更新时,更新库存时同时更新新的时间戳/版本号,并判断时间戳/版本号是否与查询时的数据一致

​ 数据库表新增字段version

ALTER TABLE `distributed_lock`.`db_stock` 
ADD COLUMN `version` int(11) NULL DEFAULT 0 COMMENT '版本号' AFTER `count`;

​ 实体类同步新增字段:version

   /**
     * 版本号
     */
    private Integer version;

​ 改造Service服务

package tech.msop.distributed.lock.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import tech.msop.distributed.lock.constants.StockConstant;
import tech.msop.distributed.lock.entity.StockEntity;
import tech.msop.distributed.lock.mapper.StockMapper;
import tech.msop.distributed.lock.service.IStockService;

import java.util.List;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 库存服务实现类 <br/>
 * 注意:@Scope的proxyMode,若为Spring原生,使用的是JDK代理,proxyMode应为INTERFACES,<br/>
 * SpringBoot 2.x起,使用的是CGlib代理,proxyMode为TARGET_CLASS
 */
@Service
@Slf4j
public class StockServiceImpl extends ServiceImpl<StockMapper, StockEntity>
        implements IStockService {
    /**
     * 减库存
     * 乐观锁不要使用事务注解
     */
    @Override
//    @Transactional
    public void checkAndLock() {
        // 1. 查询库存信息
        List<StockEntity> list = this.list(new QueryWrapper<StockEntity>().eq("product_code","1001"));
        //      这里取第一个库存
        if (CollectionUtils.isEmpty(list)) {
            return;
        }
        StockEntity stock = list.get(0);
        // 2. 判断库存是否充足
        if (stock != null && stock.getCount() > 0) {
            // 3.更新到数据库
            stock.setCount(stock.getCount() - 1);
            // 更新版本号,在原版本号的基础上加1
            Integer version = stock.getVersion();
            stock.setVersion(version + 1);
            // 判断是否更新成功,更新失败则递归调用,直至保证更新成功
            // true 表示更新行数不为null且大于等于1,false 表示更新失败
            boolean result = this.update(stock,new UpdateWrapper<StockEntity>().eq("id",stock.getId()).eq("version",version));
            if (!result){
                // 避免栈内存溢出
                try{
                    Thread.sleep(20);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
                this.checkAndLock();
            }
        }
    }

}

​ 使用Jmeter进行压力测试,并查询数据库余量:0

image-20230527134958119

注意:

  • 若需要递归调用确保数据更新成功,不要使用事务注解
    • MDL(更新、删除、新增)语句会自动加锁,重复调用可能会导致阻塞
  • 若需要递归调用确保数据更新成功,需要线程休眠一段时间,避免栈内存溢出
缺点
  • 高并发情况下,性能极低
  • ABA问题
    • 用户1查询数据X=A
    • 用户2更新数据X=B
    • 用户3更新数据X=C
    • 用户4更新数据X=A
    • 用户1更新数据时判断X是否等于A,若相同,更新X=S
      • 虽然X仍然等于A,但数据变更过
  • 读写分离情况下导致乐观锁不可靠
    • 写数据到主服务器,从服务器读取数据

MySQL锁总结

  • 性能:单SQL>悲观锁>JVM锁>乐观锁
  • 如果追求极致性能、业务场景简单并且不需要记录数据前后变化的情况下
    • 优先选择:单SQL
  • 如果写并发量较低(多读),争论不是很激烈的情况:
    • 优先选择:乐观锁
  • 如果写并发量较高,一般会经常冲突,此时选择乐观锁的话,会导致业务代码不间断的重试
    • 优先选择:悲观锁
  • 不推荐JVM本地锁

Redis乐观锁

更新Redis中的库存

​ 在Redis中新增库存:

$ set stock 5000

​ 更新StockService服务

package tech.msop.distributed.lock.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import tech.msop.distributed.lock.constants.StockConstant;
import tech.msop.distributed.lock.entity.StockEntity;
import tech.msop.distributed.lock.mapper.StockMapper;
import tech.msop.distributed.lock.service.IStockService;

import java.util.List;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 库存服务实现类 <br/>
 */
@Service
@Slf4j
public class StockServiceImpl extends ServiceImpl<StockMapper, StockEntity>
        implements IStockService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 减库存
     */
    @Override
    public void checkAndLock() {
        // 1. 查询库存信息
        String stock = redisTemplate.opsForValue().get("stock");
        // 2. 判断库存是否充足
        if (stock != null && stock.length() != 0){
            Integer st = Integer.valueOf(stock);
            if (st > 0){
                // 3.更新到数据库
                redisTemplate.opsForValue().set("stock",String.valueOf(--st));
            }
        }
    }


    public void checkAndLock2() {
        try {
            // 1.先查询库存是否充足
//            StockEntity stock = this.getOne(new QueryWrapper<StockEntity>().eq("product_code", "1001"));
//            // 2.判断库存余量
//            if (stock != null && stock.getCount() > 0) {
//                stock.setCount(stock.getCount() - 1);
//                // 3.更新到数据库
//                this.updateById(stock);
//            }

            // update insert delete写操作本身就会加锁
            // 使用一条SQL语句完成减库存操作
            // update db_stock set count = count - 1 where product_code = '1001' and count >=1
            this.baseMapper.updateStock(1, "1001");
        } finally {
        }
    }

    /**
     * 重置库存数量
     */
    @Override
    public void reset() {
        this.baseMapper.reset(StockConstant.DEFAULT_STOCK_COUNT);
    }
}

​ 使用Jmeter进行压力测试,并查询库存余量

image-20230527141527673

Redis乐观锁

watch:可以监控一个或多个key的值,如果在事务执行(exec)之前,key的值发生拜年话,则取消事务执行

multi:开启事务

exec:执行事务

​ 利用Redis监听+事务

$ watch stock
$ multi
$ set stock 5000
$ exec

​ 如果执行过程中,stock的值没有被其他链接改变,则执行成功

image-20230527141934683

​ 如果执行过程中stock的值被改变,则执行失败

image-20230527142107031

​ 更新StockService

 /**
     * 减库存
     */
    @Override
    public void checkAndLock() {
        redisTemplate.execute(new SessionCallback<Object>() {

            @Override
            public <K, V> Object execute(@NotNull RedisOperations<K, V> operations) throws DataAccessException {
                // watch
                operations.watch((K) "stock");
                // 1. 查询库存信息
                String stock = (String) operations.opsForValue().get("stock");
                // 2. 判断库存是否充足
                if (stock != null && stock.length() != 0){
                    Integer st = Integer.valueOf(stock);
                    if (st > 0){
                        // multi
                        operations.multi();
                        // 3.更新到数据库
                        operations.opsForValue().set((K) "stock", (V) String.valueOf(--st));
                        // exec 执行事务
                        List<Object> exec = operations.exec();
                        // 如果执行事务的返回结果集为空,则代表减库存失败,重试
                        if (exec ==null || exec.size() == 0){
                            try {
                                Thread.sleep(40);
                            } catch (InterruptedException e) {
                                throw new RuntimeException(e);
                            }
                            checkAndLock();
                        }
                        return exec;
                    }
                }
                return null;
            }
        });

    }

​ 使用Jmeter进行压力测试并查询库存余量:0

image-20230527143816559

缺点
  • 性能问题
  • 由于运行机器的性能问题,可能导致连接数不够用

分布式锁

跨进程、跨服务、跨服务器

分布式锁的应用场景:

  • 超卖现象(NoSQL)
  • 缓存击穿

分布式锁的实现方式:

  • 基于Redis实现
  • 基于Zookeeper/etcd实现
  • 基于MySQL实现

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/597447.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

新能源汽车充电桩的建设及优化分析

安科瑞虞佳豪 新能源汽车充电桩在经历了几年的发展之后&#xff0c;总体情况是在持续走好的&#xff0c;并且充电桩的建设相较于以往有了很大的普及度和安全度&#xff0c;这对新能源汽车车主是一个好事&#xff0c;也鼓励了更多人选择买新能源汽车&#xff0c;但这并不是说新…

HTTP劫持是什么?如何防止网站被劫持呢?

HTTP劫持&#xff08;HTTP hijacking&#xff09;是一种网络攻击技术&#xff0c;攻击者通过各种手段截取用户的HTTP请求或响应&#xff0c;篡改其内容或重定向到恶意服务器&#xff0c;从而实施恶意活动。这种攻击可能导致用户信息泄露、身份盗窃、篡改网页内容或植入恶意代码…

鼎盛合充气泵方案——便携车载充气泵方案

便携车载充气泵主要使用在汽车轮胎充气及车胎检测上&#xff0c;是一个气压精度测量产品。充气泵方案则是通过马达运转工作而进行设计&#xff0c;利用芯片和气压传感器所做的一个智能化便携车载充气泵方案。 便携车载充气泵方案的使用范围其实不仅仅是汽车轮胎&#xff0c;它在…

Android Studio Flamingo编译项目问题记录

系统版本&#xff1a;macOS 13.4 Android Studio Flamingo | 2022.2.1 Patch 2 下载地址&#xff1a;Download Android Studio & App Tools - Android DevelopersAndroid Studio provides app builders with an integrated development environment (IDE) optimized for …

protobuf笔记

protoc -Ipb/protos -Ipb/protos/third/github.com pb/protos/custom/*.proto -I 指定需要import的gogo.proto文件路径&#xff0c; protoc查找过程为 -I后面的路径和import的路径拼接在一起。 -Ipb/protos 指定proto源文件路径-Ipb/protos/third/github.com 指定第三方proto&…

el-select如何改变样式 (:popper-append-to-body=“false“)

在使用el-select的时候&#xff0c;其样式会按照Elementui自带的默认样式为基准&#xff1b; 但往往开发过程中&#xff0c;下拉框的样式可能并不是我们想要的&#xff1b;这是我遇到过的一个案例&#xff0c;开发需求上与elementui默认样式大相径庭&#xff1b; 如何进行修改呢…

JAVA基础 - CLASSLOADER双亲委派机制?

类的生命周期 在JAVA中数据类型分为基本数据类型和引用数据类型。基本数据类型&#xff0c;由虚拟机预先定义&#xff0c;引用数据类型则需要进行类加载。 JAVA将引用数据类型分为&#xff1a;类、接口、数组和泛型参数&#xff0c;而「泛型参数」在编译时期会被擦除&#xff…

web前端 --- javascript(01)-- 介绍、变量和数据类型

JavaScript w3c&#xff1a;三层分离 结构层&#xff1a;HTML 表示层&#xff1a;CSS 行为层&#xff1a;JavaScript 介绍 &#xff08;1&#xff09;作用&#xff1a; 数据校验网页特效数据交互服务器端编程&#xff08;NodeJS&#xff09; &#xff08;2&#xff09;javas…

开源赋能 普惠未来|UBSICE诚邀您参与2023开放原子全球开源峰会

UBSICE&#xff08;Unified Basic Service Infrastructure Community Edition&#xff09;是一个轻量级“面向领域”的高可用、高性能、业务连续性的微服务架构技术底座。UBSICE特有的“微服务容器”不仅是一个微服务的运行容器&#xff0c;还通过“容器控制器”管理其他微服务…

Linux超全整理Linux性能分析工具汇总

出于对Linux操作系统的兴趣&#xff0c;以及对底层知识的强烈欲望&#xff0c;因此整理了这篇文章。本文也可以作为检验基础知识的指标&#xff0c;另外文章涵盖了一个系统的方方面面。如果没有完善的计算机系统知识&#xff0c;网络知识和操作系统知识&#xff0c;文档中的工具…

大数据存储方式有哪些?

写在前面 本文隶属于专栏《大数据从 0 到 1》&#xff0c;该专栏为笔者原创&#xff0c;引用请注明来源&#xff0c;不足和错误之处请在评论区帮忙指出&#xff0c;谢谢&#xff01; 本专栏目录结构和文献引用请见《大数据从 0 到 1》 正文 数据常用的存储介质为磁盘和磁带。…

Seaborn.load_dataset()加载数据集失败最佳解决方法

load_dataset() 是 Seaborn 库中提供的一个函数&#xff0c;用于加载一些原始数据集。这些数据集包含了许多经典的数据集&#xff0c;比如鸢尾花数据集、小费数据集等&#xff0c;这些数据集在数据可视化和机器学习中非常常见。 使用 load_dataset() 函数可以方便地获取这些数…

HBase:(二)基本操作

1.数据模型 术语解释Name Space命名空间&#xff0c;类似于关系型数据库的 database 概念&#xff0c;每个命名空间下有多个表。HBase 两个自带的命名空间&#xff0c;分别是 hbase 和 default&#xff0c;hbase 中存放的是 HBase 内置的表&#xff0c;default表是用户默认使用…

数据治理8大核心模块建设

数据治理是一个去中心化、多元参与的系统工程。一个全面且明确的数据治理体系&#xff0c;可以帮助组织构建生态式、协同化治理路径&#xff0c;最大化地提升整体数据质量&#xff0c;实现数据战略&#xff0c;激活新型生产力。 本文以元数据、主数据、数据标准、数据质量、数…

2023年「身份安全」行业白皮书、研究报告、案例合集速览!

随着企业将其业务向数字化、云和移动化转变&#xff0c;身份的数量、类型都呈爆炸式增长。这也带来了全新维度的威胁格局&#xff0c;如果保护不当&#xff0c;可能会为攻击者提供更多可利用的攻击路径。 虽然许多工具和技术旨在保障身份安全&#xff0c;但身份威胁检测和响应…

chatgpt赋能python:Python主窗口名字怎么修改?

Python主窗口名字怎么修改&#xff1f; Python是一种解释型的编程语言&#xff0c;广泛应用于Web开发、数据科学、人工智能等领域。在Python编写的GUI程序中&#xff0c;窗口名字是非常重要的一个元素&#xff0c;因为它可以直观地让用户知道当前的应用程序是什么。在这篇文章…

IP地址规划方法

一、IP地址规划的基本步骤&#xff1a; &#xff08;1&#xff09;判断用户对网络以及主机数的需求&#xff1b; &#xff08;2&#xff09;计算满足用户需要的基本网络地址结构&#xff1b; &#xff08;3&#xff09;计算地址掩码&#xff1b; &#xff08;4&#xff09;…

工程swift与OC混编改造

最近公司项目准备引入swift&#xff0c;由于目前工程已经完成了组件化不再是简单的单仓工程&#xff0c;所以需要进行混编改造。下面记录一下自己对工程进行混编改造的思考以及过程。 混编原理 看了很多文档&#xff0c;比较少有讲混编原理的&#xff0c;这里简单介绍一下语言…

第十九章_手写Redis分布式锁

锁的种类 单机版同一个JVM虚拟机内synchronized或者Lock接口。 分布式多个不同JVM虚拟机&#xff0c;单机的线程锁机制不再起作用&#xff0c;资源类在不同的服务器之间共享了。 一个靠谱分布式锁需要具备的条件和刚需 独占性 &#xff1a;OnlyOne&#xff0c;任何时刻只能有且…

linux-静态库制作与使用

创建2个目录进行创建与使用的演示 创建静态库 准备源文件与头文件 查看所有源文件与头文件 将源文件编译.o文件&#xff0c;然后将.o文件打包为静态库 gcc -c mymath.c -o mymath.o -stdc99 gcc -c myprint.c -o myprint.o -stdc99 ar指令&#xff1a;打包多个.o文件为静态…