【尚硅谷】分布式锁全家桶丨一套搞定Redis/Zookeeper/MySQL实现分布式锁

163 阅读8分钟
-- Table structure for tb_stock
-- ----------------------------
DROP TABLE IF EXISTS `tb_stock`;
CREATE TABLE `tb_stock` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `product_code` varchar(20) NOT NULL,
  `warehouse` varchar(20) NOT NULL,
  `count` int(11) NOT NULL,
  `version` int(11) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_pc` (`product_code`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of tb_stock
-- ----------------------------
INSERT INTO `tb_stock` VALUES ('1', '1001', '北京仓', '0', '5009');
INSERT INTO `tb_stock` VALUES ('2', '1001', '上海仓', '4999', '0');
INSERT INTO `tb_stock` VALUES ('3', '1002', '深圳仓', '4997', '0');
INSERT INTO `tb_stock` VALUES ('4', '1002', '上海仓', '5000', '0');

接下来我们来看下代码

image.png

第一基础版本


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.11</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.atguigu</groupId>
    <artifactId>distributed-lock</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>distributed-lock</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.3.4</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>


application.properties

server.port=10010
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://192.168.175.129:3306/distributed-lock
spring.datasource.username=root
spring.datasource.password=123456

StockMapper

package com.atguigu.distributed.lock.Mapper;

import com.atguigu.distributed.lock.pojo.Stock;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;

public interface StockMapper extends BaseMapper<Stock> {
}

Stock

package com.atguigu.distributed.lock.pojo;

import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

@Data
@TableName("tb_stock")
public class Stock {

    private Long id;

    private  String productCode;


    private  String warehouse;

    private Integer count ;



}

StockService


import com.atguigu.distributed.lock.Mapper.StockMapper;
import com.atguigu.distributed.lock.pojo.Stock;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;

import java.util.concurrent.locks.ReentrantLock;

@Service
public class StockService {

    //private Stock stock = new Stock();


    @Autowired
    private StockMapper  stockMapper;

    private ReentrantLock lock = new ReentrantLock();

    public void deduct(){
        lock.lock();
        try {

            Stock stock = stockMapper.selectOne(new QueryWrapper<Stock>().eq("product_code","1001"));
            if(stock!=null && stock.getCount() > 0){
                stock.setCount(stock.getCount()-1);
                stockMapper.updateById(stock);
            }


        } finally {
            lock.unlock();
        }
    }
}


StockController


import com.atguigu.distributed.lock.service.StockService;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@MapperScan("com.atguigu.distributed.lock.Mapper")
public class StockController {

    @Autowired
    private StockService stockService;

    @GetMapping("stock/deduct")
    public String deduct(){
        this.stockService.deduct();
        return "hello stock deduct!!";
    }

}


上述代码就允许起来了,但是上述代码可能是存在超卖的情况出现的,在多线程的情况下,如何解决了。

第一种解决办法使用一个sql语句搞定

update tb_stock set count = count -1 where product_code= '1001' and count > 0;

通过一个sql语句来解决上面的问题,在跟新的时候首先判断count是否库存大于0,如果库存大于0,才进行更新

接下来我们来跟新代码 StockMapper

package com.atguigu.distributed.lock.Mapper;

import com.atguigu.distributed.lock.pojo.Stock;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;

public interface StockMapper extends BaseMapper<Stock> {

    @Update("update tb_stock  set count=count - #{count} where product_code=#{productCode} and count > 0")
    int updatestock (@Param("productCode") String productCode, @Param("count")Integer count);
}

StockService

package com.atguigu.distributed.lock.service;

import com.atguigu.distributed.lock.Mapper.StockMapper;
import com.atguigu.distributed.lock.pojo.Stock;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;

import java.util.concurrent.locks.ReentrantLock;

@Service
public class StockService {

    //private Stock stock = new Stock();


    @Autowired
    private StockMapper  stockMapper;

    private ReentrantLock lock = new ReentrantLock();





    public void deduct2(){

        try {

            stockMapper.updatestock("1001",1);


        } finally {

        }
    }
}

经过验证之后,就能够解决上面在多实例、多线程下超卖的问题,但是使用单独的一个sql语句,还存在一定的问题 就是整个语句到底是表锁,还是行锁的问题,要提高整个线程的并发性

我们可以采用下面的方式进行验证

线程1:执行更新1001的库存操作,在更新操作未提交事务之前,在开启一个线程2,更新1002操作,默认情况下,可以看到在1001的更新未提交事务之前,线程2更新1002的操作是阻塞的,默认情况下是表锁,把整个表锁定的。

image.png

image.png

线程1:执行更新1001的库存操作,在更新操作未提交事务之前,在开启一个线程2,更新1002操作,默认情况下,可以看到在1001的更新未提交事务之前,线程2更新1002的操作是阻塞的,默认情况下是表锁,把整个表锁定的。

默认情况是表锁,要提高效率,变成行锁,如何解决了

image.png

只需要个给更新和查询的字段创建索引就可以了,所以只需要给tb_stock表中的更新字段product_code创建一个索引就可以了;

-- ----------------------------
DROP TABLE IF EXISTS `tb_stock`;
CREATE TABLE `tb_stock` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `product_code` varchar(20) NOT NULL,
  `warehouse` varchar(20) NOT NULL,
  `count` int(11) NOT NULL,
  `version` int(11) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_pc` (`product_code`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;

在创建索引成功之后接下来我们来看下面的操作;

线程1更新编号为1001的记录,线程2同时更新编号为1002的记录,变成行锁之后,两个不同行记录的操作是互不影响的。

image.png

线程2

image.png

第二个条件,查询的条件必须是具体的值,不能是模糊的值,如下面使用!=或者like表达式,就会导致整个表都会被锁 如下

image.png

线程2操作的时候,会一直被阻塞,直到线程1事务提交,是一个表锁

image.png

所以在数据库的表的更新操作中一定要注意表锁和行锁,这里一定要非常的注意。

第二种解决方案:select for update 悲观锁

我们首先做一个直观的操作,让大家知道这个效果

在线程1上面开启一个 for update 操作

image.png

在线程1上面开启一个 for update 操作,更新商品编号为1001的操作,如果线程1没有提交commit操作,线程2开启一个任务执行也更新编号为1001的操作,操作同一个行记录,因为线程1没有执行commit操作,这个时候线程2是无法操作成功的,必须等待线程1的操作完成提交后,线程2的更新的操作才能完成,for update是能够保证行锁操作的,这就是原理

image.png

使用for update悲观锁实现并发操作,需要注意下面的几个事情

悲观锁: 比较悲观,一旦加锁,自身增删查改,其他线程无法任何操作,不能与其他锁并存。加锁方式 for update for update 来解决并发重复查询,保证每次只有只能一个线程执行查询

for update 来解决并发重复查询,保证每次只有只能一个线程执行查询  

** 观锁,正如其名,具有强烈的独占和排他特性。上来就锁住,把事情考虑的比较悲观,它是采用数据库机制实现的,数据库被锁之后其它用户将无法查看,直到提交或者回滚,锁释放之后才可查看。所以悲观锁具有其霸道性。

     简单说其悲观锁的功能就是,锁住读取的记录,防止其它事物读取和更新这些记录,而其他事物则会一直堵塞,知道这个事物结束。**

使用悲观锁要注意下面的两个条件: 1.悲观锁:假设每一次拿数据,都有认为会被修改,所以给数据库的行或表上锁。要注意for update要用在索引上,不然会锁表。类似上面的一个sql语句,需要叫上更新和查询的字段要叫上索引; 2.就是一定要在事务中执行

image.png 转载自博客:cloud.tencent.com/developer/a…

image.png

接下来我们通过代码来实现

StockMapper
package com.atguigu.distributed.lock.Mapper;

import com.atguigu.distributed.lock.pojo.Stock;
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 java.util.List;

public interface StockMapper extends BaseMapper<Stock> {

    @Update("update tb_stock  set count=count - #{count} where product_code=#{productCode} and count > 0")
    int updatestock (@Param("productCode") String productCode, @Param("count")Integer count);



 **   @Select("select * from tb_stock where product_code=#{productCode}  for update")
    List<Stock> querystock(String productCode);**
}

业务层级的代码如下所示:

package com.atguigu.distributed.lock.service;

import com.atguigu.distributed.lock.Mapper.StockMapper;
import com.atguigu.distributed.lock.pojo.Stock;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

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

@Service
public class StockService {

    //private Stock stock = new Stock();


    @Autowired
    private StockMapper  stockMapper;




    @Transactional
    public void deduct2(){

        try {

            List<Stock> stockList = stockMapper.querystock("1001");
            if (stockList!= null){
                Stock stock = stockList.get(0);
                if(stock != null &&  stock.getCount() > 0){
                    stock.setCount(stock.getCount()-1);
                    stockMapper.updateById(stock);
                }
            }


        } finally {

        }
    }
}

image.png

第四章方案,乐观锁实现高并发

乐观锁:就是很乐观,每次去拿数据的时候都认为别人不会修改。更新时如果version变化了,更新不会成功。

update status set name='nike',version=(version+1) where id=1 and version=version;

接下来我们来看代码的实现

image.png


import com.atguigu.distributed.lock.Mapper.StockMapper;
import com.atguigu.distributed.lock.pojo.Stock;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

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

@Service
public class StockService {

    //private Stock stock = new Stock();


    @Autowired
    private StockMapper  stockMapper;

    private ReentrantLock lock = new ReentrantLock();





    //乐观锁实现
    public void deduct22w() throws InterruptedException {

        try {

            // 1. 查询库存
            List<Stock> stockList = stockMapper.selectList(new QueryWrapper<Stock>().eq("product_code", "1001"));

            //2.  这里选择第一个库存

            if(stockList !=null && stockList.size() > 0){

                Stock stock = stockList.get(0);

                // 3.判断库存是否充足,然后扣减库存

                if(stock !=null && stock.getCount() > 0){

                    stock.setCount(stock.getCount() -1);

                    Integer version = stock.getVersion();

                    // 4.更新乐观锁的vesion

                    stock.setVersion(version+1);

                    // 依据version 更新数据库

                    int update = stockMapper.update(stock, new UpdateWrapper<Stock>().eq("id", stock.getId()).eq("version", version));


                    // update表示更新的行数记录,如果大于1 ,说明更新成功

                    if(update == 0) {
                        //说明更新失败,需要自旋,重新获取锁,再次进行跟新

                        Thread.sleep(20);

                        this.deduct22w();

                    }else
                    {
                        System.out.println("获取锁成功,完成库存扣减");
                    }

                }

            }




        } finally {

        }
    }
}


1.乐观锁的并发效率在jemter压测情况下只有200,乐观锁的并发效率还没有悲观锁的高。 2.乐观锁底层就是依赖数据库的锁资源,千万不能再代码层面添加@Transactional,乐观锁不需要 @Transactional注解

image.png

2.乐观锁底层就是依赖数据库的锁资源,千万不能再代码层面添加@Transactional,乐观锁不需要 @Transactional注解,如果使用了该注解会报下面的错误

image.png

image.png

3.乐观锁在主从架构下存在问题,version在主节点上面已经跟新了,但是因为网络原因,在从节点上面还是原来的version的值,所以会导致更新成功,被重复跟新。