分布式锁(第一部分)

分布式锁学习图:


1、什么是分布式锁,为什么需要它?

所谓的锁,在操作系统中了解过,就是为了避免对临界资源的访问而出现资源不一致性的问题,必须要是哦也能够互斥机制来保证资源的一致性。

在单体项目中,一般是使用JVM层面的锁实现资源的互斥,那就是Synchronized和ReentrantLock同步锁实现

但是如果在分布式的微服务里面,有多个计算机,各个计算机之间是跨JVM的,那么这些同步锁就不能实现,以至于失效,所以需要一个全局的锁,这个全局的锁,他不会特意的在哪一个计算机或者服务器里面提供服务,而是抽取出来,作为一个全局资源的把控,比如,如图:


该图就是一个分布式的系统,现在有两个服务器处理下订单的业务,如果不加分布式锁的话,结果就是:数据库的商品库存总共就1000件,但是在同一时刻有两个请求进入到了服务器1和服务器2,都想要下订单1000件,结果就是卖出去了2000件,但是数据库就只有1000件,所以出现问题了,就算你添加了Synchronized和ReentrantLock锁也是无济于事。

而分布式锁他应该是这样的,如图:


会发现,在服务器之间是有一个分布式锁的存在,在真正处理订单业务的时候,会进行获取锁,如果可以获得锁的话,就继续执行创建订单,反之,则需要等待。

分布式锁,在任意一个时刻,都是只有一个线程使用锁,其余的线程全部等待。

2、在单体项目下抢购订单(不加锁)

第一、创建数据库
// 订单表
CREATE TABLE `t_order` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `productid` int(11) DEFAULT NULL,
  `count` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=41 DEFAULT CHARSET=utf8;

// 商品表
CREATE TABLE `t_product` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `count` int(11) DEFAULT NULL,
  `price` int(10) DEFAULT NULL,
  `name` varchar(22) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=10002 DEFAULT CHARSET=utf8;
第二、创建pojo实体对象
// 创建商品实体类
/**
 * @Description: 商品实体类
 * @Author: huidou 惠豆
 * @CreateTime: 2022/6/12 10:36
 */
@Data
public class Product {

    private Integer id;

    private Integer count;

    private Integer price;

    private String name;

}

// 创建订单实体类
/**
 * @Description: 订单实体类
 * @Author: huidou 惠豆
 * @CreateTime: 2022/6/12 10:35
 */
@Data
public class Order {

    private Integer id;

    private Integer productid;

    private Integer count;

}
第三、项目依赖
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.3</version>
    </parent>

    <dependencies>
        <!--  mysql driver  -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!--  mybatis  -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.1</version>
        </dependency>
        <!--  lombok  -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.22</version>
        </dependency>
        <!--  web  -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--  thymeleaf  -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <!--  redis  -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
    </dependencies>
第四、创建MVC三层结构
// controller层
@RestController
@RequestMapping("/order")
public class OrderController {

    @Autowired
    private ProductService productService;

    @Autowired
    private OrderService orderService;


    /**
     * 创建一个订单
     * @param id
     * @param count
     * @return
     */
    @Transactional(rollbackFor = Exception.class)
    @GetMapping("/createOrder")
    public String createOrder(Integer id,Integer count) {
        // 1、根据商品ID查询出商品信息
        Product product = productService.selectProduct(id);
        if (product == null) {
            return "该商品不存在";
        }
        // 2、根据购买的数量进行判断是否合理
        if (product.getCount() < count) {
            return "商品的库存不足,无法购买";
        }
        // 3、创建订单,并且将其信息插入数据库中
        Order insertOrder = new Order();
        insertOrder.setId(12);
        insertOrder.setProductid(id);
        insertOrder.setCount(count);
        Integer insertResult = orderService.addOrder(insertOrder);
        // 4、修改商品表的库存数量信息
        Product updateProduct = new Product();
        updateProduct.setCount(product.getCount() - count);
        Integer updateResult = productService.consumerProduct(product.getId(),count);
        // 5、判断操作
        if (insertResult >= 1 && updateResult >= 1) {
            return "创建订单信息成功";
        } else {
            return "创建订单信息失败";
        }
    }

}   

// service层
/**
 * @Description: 商品服务接口
 * @Author: huidou 惠豆
 * @CreateTime: 2022/6/12 10:41
 */
public interface ProductService {
    // 1、消费商品
    Integer consumerProduct(Integer id,Integer count);
    // 2、根据商品ID,查询商品信息
    Product selectProduct(Integer id);
}
/**
 * @Description: 订单服务接口
 * @Author: huidou 惠豆
 * @CreateTime: 2022/6/12 10:40
 */
public interface OrderService {
    // 1、创建订单
    Integer addOrder(Order order);
}

-----------------------------------------------
/**
 * @Description: 订单服务接口实现类
 * @Author: huidou 惠豆
 * @CreateTime: 2022/6/12 10:43
 */
@Service
public class OrderServiceImpl implements OrderService {

    @Autowired
    private OrderMapper orderMapper;

    /**
     * 添加一个订单
     * @param order
     * @return
     */
    @Override
    public Integer addOrder(Order order) {
        return orderMapper.insert(order);
    }
}

/**
 * @Description: 商品服务接口实现类
 * @Author: huidou 惠豆
 * @CreateTime: 2022/6/12 10:44
 */
@Service
public class ProductServiceImpl implements ProductService {

    @Autowired
    private ProductMapper productMapper;

    /**
     * 消费商品
     * @param id
     * @return
     */
    @Override
    public Integer consumerProduct(Integer id,Integer count) {
        return productMapper.updateById(id,count);
    }

    /**
     * 根据商品ID,查询商品信息
     * @param id
     * @return
     */
    @Override
    public Product selectProduct(Integer id) {
        return productMapper.selectById(id);
    }
}

// mapper层

/**
 * @Description: 订单mapper
 * @Author: huidou 惠豆
 * @CreateTime: 2022/6/12 10:38
 */
@Repository
public interface OrderMapper {
//  创建一个订单
    Integer insert(Order order);
}

/**
 * @Description: 商品实体类
 * @Author: huidou 惠豆
 * @CreateTime: 2022/6/12 10:39
 */
@Repository
public interface ProductMapper{
    /**
     * 根据商品的id修改商品信息
     * @param id
     * @return
     */
    Integer updateById(Integer id,Integer count);
    /**
     * 根据商品id查询商品信息
     * @param id
     * @return
     */
    Product selectById(Integer id);
}
第五、配置文件application.yml
spring:
  application:
    name: DistributedLock

  datasource:
    url: jdbc:mysql://localhost:3306/school
    username: root
    password: root

mybatis:
  mapper-locations: classpath:/mapper/*.xml
第六、Mapper映射配置文件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="com.hui.mapper.OrderMapper">

<!--    1、 创建一个订单   -->
    <insert id="insert" parameterType="com.hui.pojo.Order">
        insert into t_order values (default ,#{productid},#{count})
    </insert>

</mapper>

<?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="com.hui.mapper.ProductMapper">

<!--    1、selectById 据商品id查询商品信息  -->
    <select id="selectById" resultType="com.hui.pojo.Product">
        select * from t_product where id = #{id}
    </select>

<!--    2、根据商品的id修改商品信息 -->
    <update id="updateById" parameterType="int">
        update t_product set count = count - #{count} where id = #{id}
    </update>
</mapper>
上面的情况在Jmeter压力测试中是不能正确创建订单的,会导致订单数量不正确,出现超卖,商品数量为负数的可能性。

3、在单体项目下抢购订单(加锁)

单体项目加锁可以使用Synchronized或者ReentrantLock锁实现数据的互斥访问。
第一、使用Synchronized锁
/**
     * 创建一个订单
     * @param id
     * @param count
     * @return
     */
    @Transactional(rollbackFor = Exception.class)
    @GetMapping("/createOrder")
    public synchronized String createOrder(Integer id,Integer count) {
        // 1、根据商品ID查询出商品信息
        Product product = productService.selectProduct(id);
        if (product == null) {
            return "该商品不存在";
        }
        // 2、根据购买的数量进行判断是否合理
        if (product.getCount() < count) {
            return "商品的库存不足,无法购买";
        }
        // 3、创建订单,并且将其信息插入数据库中
        Order insertOrder = new Order();
        insertOrder.setId(12);
        insertOrder.setProductid(id);
        insertOrder.setCount(count);
        Integer insertResult = orderService.addOrder(insertOrder);
        // 4、修改商品表的库存数量信息
        Product updateProduct = new Product();
        updateProduct.setCount(product.getCount() - count);
        Integer updateResult = productService.consumerProduct(product.getId(),count);
        // 5、判断操作
        if (insertResult >= 1 && updateResult >= 1) {
            return "创建订单信息成功";
        } else {
            return "创建订单信息失败";
        }
    }


4、在分布式项目下抢购订单(在加锁的前提下,暴露分布式锁的必要性)

第一、看代码
/**
     * 创建一个订单
     * @param id
     * @param count
     * @return
     */
    @Transactional(rollbackFor = Exception.class)
    @GetMapping("/createOrder")
    public synchronized String createOrder(Integer id,Integer count) {

        // 1、根据商品ID查询出商品信息
        Product product = productService.selectProduct(id);
        if (product == null) {
            return "该商品不存在";
        }
        // 2、根据购买的数量进行判断是否合理
        if (product.getCount() < count) {
            return "商品的库存不足,无法购买";
        }
        // 3、创建订单,并且将其信息插入数据库中
        Order insertOrder = new Order();
        insertOrder.setId(12);
        insertOrder.setProductid(id);
        insertOrder.setCount(count);
        Integer insertResult = orderService.addOrder(insertOrder);
        // 4、修改商品表的库存数量信息
        Product updateProduct = new Product();
        updateProduct.setCount(product.getCount() - count);
        Integer updateResult = productService.consumerProduct(product.getId(),count);
        // 5、判断操作
        if (insertResult >= 1 && updateResult >= 1) {
            return "创建订单信息成功";
        } else {
            return "创建订单信息失败";
        }
    }
第二、设计nginx网关
    upstream test{

      server localhost:8080 ;
      server localhost:8081 ;

    }

	server {
        listen       80;
        server_name  localhost;
        location / {
            proxy_pass http://test;
        }
        location = /50x.html {
            root   html;
        }
    }

看到这里大家会发现,我的代码方法是添加了同步锁Synchronized,那么看看Jmeter测试之后的结果吧!

设置数据库库存为2,请求由1000个,查看最后的执行结果:



会发现居然超买了,这是为啥?分为两个原因:

  • 第一,就是我之前所说的JVM层面的锁只适合在单体的项目中,对于分布式的请求是不适合的

  • 第二,就是Synchronized与注解@Transactional配合有问题,这是针对于在同一个机器中,当前一个前一个请求占了所之后,会进行处理,处理完成之后,退出方法,这是@Transactional注解才会去提交事务,但是这个时候,第二个请求已经进来了,所以就会导致数据的脏读

这个问题如何解决呢?

  • 对于分布式请求们就需要分布式解决方案处理,具体看后面描述。

  • 对于单体的问题。首先,添加锁是一定的,因为有可能同一个机器同时接收了很多的请求,这时就需要同一台JVM对请求进行控制,而对于Synchronized与注解@Transactional配合的问题,则需要使用事务管理器来处理,实现手动的提交事务,即可解决。

@RestController
@RequestMapping("/order")
public class OrderController {

    @Autowired
    private ProductService productService;

    @Autowired
    private OrderService orderService;

    // 声明事务管理器
    @Autowired
    private PlatformTransactionManager platformTransactionManager;

    // 声明事务定义
    @Autowired
    private TransactionDefinition transactionDefinition;

    /**
     * 创建一个订单
     * @param id
     * @param count
     * @return
     */
//    @Transactional(rollbackFor = Exception.class)
    @GetMapping("/createOrder")
    public synchronized String createOrder(Integer id,Integer count) {
        // 手动创建事务
        TransactionStatus transaction = platformTransactionManager.getTransaction(transactionDefinition);
        // 1、根据商品ID查询出商品信息
        Product product = productService.selectProduct(id);
        if (product == null) {
            platformTransactionManager.rollback(transaction);
            return "该商品不存在";
        }
        // 2、根据购买的数量进行判断是否合理
        if (product.getCount() < count) {
            platformTransactionManager.rollback(transaction);
            return "商品的库存不足,无法购买";
        }
        // 3、创建订单,并且将其信息插入数据库中
        Order insertOrder = new Order();
        insertOrder.setId(12);
        insertOrder.setProductid(id);
        insertOrder.setCount(count);
        Integer insertResult = orderService.addOrder(insertOrder);
        // 4、修改商品表的库存数量信息
        Product updateProduct = new Product();
        updateProduct.setCount(product.getCount() - count);
        Integer updateResult = productService.consumerProduct(product.getId(),count);
        // 5、提交事务
        platformTransactionManager.commit(transaction);
        return "事务提交成功---订单创建成功";
    }

}
会发现,需要注入一个事务管理器PlatformTransactionManager和事务定义TransactionDefinition,然后在执行方法中,手动创建事务,手动提交与手动撤销事务。
全部评论

相关推荐

点赞 评论 收藏
分享
评论
点赞
1
分享
牛客网
牛客企业服务