1、常见的分布式事务锁

1、数据库级别的锁

  • 乐观锁,给予加入版本号实现
  • 悲观锁,基于数据库的for update实现

2、Redis,基于SETNX、EXPIRE实现

3、Zookeeper,基于InterProcessMutex实现

4、Redisson的lock、tryLock(背后原理也是Redis)

2、redis搭建模式

单机: 只有一台,挂了就无法工作。主从:备份关系,数据会同步到从库,可以读写分离。哨兵:master挂了,哨兵就进行选举,选出新的master,作用是监控主从,主从切换。集群:高可用,分散请求,目的是将数据分片存储,节省内内存。







分布式事务: 按照传统的系统架构、下单、扣库存等,这一系列的操作都是在一个应用一个数据库中完成的,也就是要保证了事务的ACID特性。如果在分布式应用中就会涉及到跨应用、跨库。这样就涉及到了分布式事务,就要考虑怎么保证这一列操作要么都成功要么都失败。保证数据的一致性

3、redis分布式锁的原理

互斥性:保证同一时间只有一个客户端可以拿到锁

安全性:只有加锁的服务才有解锁权限,也就是不能让客户端A加的锁,客户端B、C都可以解锁

避免死锁:保证加锁与解锁操作是原子操作,这个其实属于是实现分布式锁的问题,假设a用redis实现分布式锁,假设加锁操作,操作步骤分为两步:1,设置key set(key,value) 2,给key设置过期时间。

Redis实现分布式锁的核心就是

加锁

  1. SET key value NX EX timeOut

参数说明:

NX:只有这个key不存才的时候才会进行操作,即 if not exists;

EX:设置key的过期时间为秒,具体时间由第5个参数决定

timeOut:设置过期时间保证不会出现死锁【避免宕机死锁】

代码实现:

  1. public Boolean lock(String key,String value,Long timeOut){
  2. String var1 = jedis.set(key,value,"NX","EX",timeOut); //加锁,设置超时时间 原子性操作
  3. if(LOCK_SUCCESS.equals(var1)){
  4. return true;
  5. }
  6. return false;
  7. }

总的来说,执行上面的set()方法就只会导致两种结果:

当前没有锁(key不存在),那么就进行加锁操作,并对锁设置个有效期,同时value表示加锁的客户端。已有锁存在,不做任何操作。

注:从2.6.12版本后, 就可以使用set来获取锁、Lua 脚本来释放锁。setnx是以前刚开始的实现方式,set命令nx、xx等参数,,就是为了实现 setnx 的功能。

解锁:

代码实现:

  1. public Boolean redisUnLock(String key, String value) {
  2. String luaScript = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
  3. Object var2 = jedis.eval(luaScript, Collections.singletonList(key), Collections.singletonList(value));
  4. if (UNLOCK_SUCCESS == var2) {
  5. return true;
  6. }
  7. return false;
  8. }

这段lua代码的意思:首先获取锁对应的value值,检查是否与输入的value相等,如果相等则删除锁(解锁)。

4、Redisson 分布式锁原理(重要)

Redisson是一个在Redis的基础上实现的Java驻内存数据网格。

加锁流程



redisson的lock()、tryLock()方法 底层 其实是发送一段lua脚本到一台服务器:

  1. if (redis.call('exists' KEYS[1]) == 0) then + -- exists 判断key是否存在
  2. redis.call('hset' KEYS[1] ARGV[2] 1); + --如果不存在,hset存哈希表
  3. redis.call('pexpire' KEYS[1] ARGV[1]); + --设置过期时间
  4. return nil; + -- 返回null 就是加锁成功
  5. end; +
  6. if (redis.call('hexists' KEYS[1] ARGV[2]) == 1) then + -- 如果key存在,查看哈希表中是否存在(当前线程)
  7. redis.call('hincrby' KEYS[1] ARGV[2] 1); + -- 给哈希中的key1,代表重入1次,以此类推
  8. redis.call('pexpire' KEYS[1] ARGV[1]); + -- 重设过期时间
  9. return nil; +
  10. end; +
  11. return redis.call('pttl' KEYS[1]); --如果前面的if都没进去,说明ARGV[2]的值不同,也就是不是同一线程的锁,这时候直接返回该锁的过期时间

参数说明:

KEYS[1]:即加锁的key,RLock lock = redisson.getLock("myLock"); 中的myLock

ARGV[1]:即 TimeOut 锁key的默认生存时间,默认30秒

ARGV[2]:代表的是加锁的客户端的ID,类似于这样的:99ead457-bd16-4ec0-81b6-9b7c73546469:1

其中lock()默认是30秒的生存时间。

锁互斥

假如客户端A已经拿到了 myLock,现在 有一客户端(未知) 想进入:

1、第一个if判断会执行“exists myLock”,发现myLock这个锁key已经存在了。

2、第二个if判断,判断一下,myLock锁key的hash数据结构中, 如果是客户端A重新请求,证明当前是同一个客户端同一个线程重新进入,所以可从入标志+1,重新刷新生存时间(可重入); 否则进入下一个if。

3、第三个if判断,客户端B 会获取到pttl myLock返回的一个数字,这个数字代表了myLock这个锁key的剩余生存时间。比如还剩15000毫秒的生存时间。

此时客户端B会进入一个while循环,不停的尝试加锁。

watch dog 看门狗自动延期机制

lockWatchdogTimeout(监控锁的看门狗超时,单位:毫秒)

默认值:30000

监控锁的看门狗超时时间单位为毫秒。该参数只适用于分布式锁的加锁请求中未明确使用leaseTimeout参数的情况。(如果设置了leaseTimeout那就会自动失效了呀~)

看门狗的时间可以自定义设置:

  1. config.setLockWatchdogTimeout(30000);

看门狗有什么用呢?

假如客户端A在超时时间内还没执行完毕怎么办呢? redisson于是提供了这个看门狗,如果还没执行完毕,监听到这个客户端A的线程还持有锁,就去续期,默认是 LockWatchdogTimeout/ 3 即 10 秒监听一次,如果还持有,就不断的延长锁的有效期(重新给锁设置过期时间,30s)

可以在lock的参数里面指定:

  1. lock.lock(); //如果不设置,默认的生存时间是30s,启动看门狗
  2. lock.lock(10, TimeUnit.SECONDS);//10秒以后自动解锁,不启动看门狗,锁到期不续

如果是使用了可重入锁( leaseTimeout):

  1. lock.tryLock(); //如果不设置,默认的生存时间是30s,启动看门狗
  2. lock.tryLock(100, 10, TimeUnit.SECONDS);//尝试加锁最多等待100秒,上锁以后10秒自动解锁,不启动看门狗

这里的第二个参数leaseTimeout 设置为 10 就会覆盖 看门狗的设置(看门狗无效),在10秒后锁就自动失效,不会去续期;如果是 -1 ,就表示 使用看门狗的默认值。

释放锁机制

lock.unlock(),就可以释放分布式锁。就是每次都对myLock数据结构中的那个加锁次数减1。

如果发现加锁次数是0了,说明这个客户端已经不再持有锁了,此时就会用:“del myLock”命令,从redis里删除这个key。

  • 为了安全,会先校验是否持有锁再释放,防止业务执行还没执行完,锁到期了。(此时没占用锁,再unlock就会报错)
  • 主线程异常退出、或者假死
  1. finally {
  2. if (rLock.isLocked()) {
  3. if (rLock.isHeldByCurrentThread()) {
  4. rLock.unlock();
  5. }
  6. }
  7. }

可能存在的问题

如果是 主从、哨兵模式,当客户端A 把 myLock这个锁 key 的value写入了 master,此时会异步复制给slave实例。

万一在这个主从复制的过程中 master 宕机了,主备切换,slave 变成了master。

那么这个时候 slave还没来得及加锁,此时 客户端A的myLock的 值是没有的,客户端B在请求时,myLock却成功为自己加了锁。这时候分布式锁就失效了,就会导致数据有问题。

所以说Redis分布式说最大的缺点就是宕机导致多个客户端加锁,导致脏数据,不过这种几率还是很小的。

5、实际应用(重要),模拟秒杀任务

引入依赖

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-data-redis</artifactId>
  4. </dependency>
  5. <dependency>
  6. <groupId>org.redisson</groupId>
  7. <artifactId>redisson</artifactId>
  8. <version>3.7.3</version>
  9. </dependency>

yml文件redis配置

  1. spring:
  2. redis:
  3. database: 0
  4. host: 127.0.0.1
  5. port: 6124

配置redisson

  1. @Configuration
  2. public class RedissonConfig {
  3. @Bean
  4. public RedissonClient redissonClient() {
  5. Config config = new Config();
  6. // config.useSingleServer();//单机
  7. // config.useMasterSlaveServers();//集群
  8. // config.useSentinelServers();//哨兵
  9. // config.useClusterServers();//集群
  10. // config.setLockWatchdogTimeout(30000);
  11. //使用的Redis主从模式
  12. config.useMasterSlaveServers()
  13. .setPassword("redis")
  14. .setMasterAddress("redis://82.71.16.139:6379")
  15. .addSlaveAddress("redis://82.71.16.139:6380","redis://82.71.16.139:6381");
  16. return Redisson.create(config);
  17. }

新建两个实体

  1. /**
  2. * @author 公众号:HelloCoder,每天分享Java技术和面试题
  3. * @date 2020/10/16
  4. * @Description
  5. */
  6. @Builder
  7. @Data
  8. @TableName("t_book")
  9. @AllArgsConstructor
  10. @NoArgsConstructor
  11. public class Book {
  12. @TableId(value = "book_id", type = IdType.AUTO)
  13. private long bookId;
  14. private String name;
  15. private int count;
  16. }
  1. @Builder
  2. @Data
  3. @TableName("t_book_order")
  4. @AllArgsConstructor
  5. @NoArgsConstructor
  6. public class Order {
  7. @TableId(value = "id", type = IdType.AUTO)
  8. private int id;
  9. private String orderId;
  10. private long bookId;
  11. private int status;
  12. private long userId;
  13. private int count;
  14. private String billTime;
  15. }
  1. @RestController
  2. @Slf4j
  3. @RequestMapping("Order/")
  4. public class OrderController {
  5. @Autowired
  6. BookOrderService bookOrderService;
  7. @RequestMapping("/seckill")
  8. public RetResult seckill(@RequestParam(value = "bookId") Long bookId, @RequestParam(value = "userId", required = false) Long userId) {
  9. if (userId == null) {
  10. //模拟userId,随机生成,这里应该有前端传入
  11. userId = (long) (Math.random() * 1000);
  12. }
  13. String result = bookOrderService.seckill(bookId, userId);
  14. return RetResponse.makeOKRsp(result);
  15. }
  16. }

这里模拟了两种情况:

一种是不加锁,第二种是加redis锁

  1. @Slf4j
  2. @Service
  3. public class BookOrderService {
  4. @Autowired
  5. BookMapper bookMapper;
  6. @Autowired
  7. OrderMapper orderMapper;
  8. @Autowired
  9. RedissonClient redissonClient;
  10. public String seckill(Long bookId, Long userId) {
  11. return notLockDemo(bookId, userId);
  12. // return lockDemo(bookId, userId);
  13. }
  14. String lockDemo(Long bookId, Long userId) {
  15. final String lockKey = bookId + ":" + "seckill" + ":RedissonLock";
  16. RLock rLock = redissonClient.getLock(lockKey);
  17. try {
  18. // 尝试加锁,最多等待20秒,上锁以后10秒自动解锁
  19. Boolean flag = rLock.tryLock(20, 10, TimeUnit.SECONDS);
  20. if (flag) {
  21. //1、判断这个用户id 是否已经秒杀过
  22. List<Order> list = orderMapper.selectList(new QueryWrapper<Order>().lambda().eq(Order::getUserId, userId).eq(Order::getStatus, 1).eq(Order::getBookId, bookId));
  23. if (list.size() >= 1) {
  24. log.info("你已经抢过了");
  25. return "你已经抢过了,一人只能抢一次";
  26. }
  27. //2、查库存
  28. Book book = bookMapper.selectOne(new QueryWrapper<Book>().lambda().eq(Book::getBookId, bookId));
  29. if (book != null && book.getCount() > 0) {
  30. //生成订单
  31. String orderId = UUID.randomUUID().toString();
  32. Order newOrder = Order.builder().
  33. orderId(orderId).
  34. status(1).
  35. bookId(bookId).
  36. userId(userId).
  37. count(1).
  38. billTime(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())).build();
  39. orderMapper.insert(newOrder);
  40. //更新库存
  41. Book newBook = Book.builder().count(book.getCount() - 1).build();
  42. bookMapper.update(newBook, new QueryWrapper<Book>().lambda().eq(Book::getBookId, bookId));
  43. log.info("userId:{} 秒杀成功", userId);
  44. return "秒杀成功" + "";
  45. } else {
  46. log.info("秒杀失败,被抢完了");
  47. }
  48. } else {
  49. log.info("请勿重复点击,userid:{} ", userId);
  50. return "你已经抢过了";
  51. }
  52. } catch (Exception e) {
  53. e.printStackTrace();
  54. } finally {
  55. if (rLock.isLocked()) {
  56. if (rLock.isHeldByCurrentThread()) {
  57. rLock.unlock();
  58. }
  59. }
  60. }
  61. return "很遗憾,没货了...";
  62. }
  63. String notLockDemo(Long bookId, Long userId) {
  64. //1、判断这个用户id 是否已经秒杀过
  65. List<Order> list = orderMapper.selectList(new QueryWrapper<Order>().lambda().eq(Order::getUserId, userId).eq(Order::getStatus, 1).eq(Order::getBookId, bookId));
  66. if (list.size() >= 1) {
  67. log.info("你已经抢过了");
  68. return "你已经抢过了,一人只能抢一次";
  69. }
  70. //2、查库存
  71. Book book = bookMapper.selectOne(new QueryWrapper<Book>().lambda().eq(Book::getBookId, bookId));
  72. if (book != null && book.getCount() > 0) {
  73. //生成订单
  74. String orderId = UUID.randomUUID().toString();
  75. Order newOrder = Order.builder().
  76. orderId(orderId).
  77. status(1).
  78. bookId(bookId).
  79. userId(userId).
  80. count(1).
  81. billTime(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())).build();
  82. orderMapper.insert(newOrder);
  83. //更新库存
  84. Book newBook = Book.builder().count(book.getCount() - 1).build();
  85. bookMapper.update(newBook, new QueryWrapper<Book>().lambda().eq(Book::getBookId, bookId));
  86. log.info("userId:{} 秒杀成功", userId);
  87. return "秒杀成功" + "";
  88. } else {
  89. log.info("秒杀失败,被抢完了");
  90. return "很遗憾,没货了...";
  91. }
  92. }
  93. }

新建两个表

  1. DROP TABLE IF EXISTS `t_book` ;
  2. CREATE TABLE `t_book` (
  3. `book_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '编号',
  4. `name` varchar(400) DEFAULT NULL COMMENT '名称',
  5. `count` int DEFAULT 0 COMMENT '数量',
  6. PRIMARY KEY (`book_id`) USING BTREE
  7. ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='商品表';
  8. DROP TABLE IF EXISTS `t_book_order` ;
  9. CREATE TABLE `t_book_order` (
  10. `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '编号',
  11. `order_id` varchar(100) NOT NULL COMMENT '订单号',
  12. `book_id` bigint(20) NOT NULL COMMENT '商品id',
  13. `user_id` bigint(20) DEFAULT NULL COMMENT '用户id',
  14. `status` int DEFAULT 1 COMMENT '状态',
  15. `count` int DEFAULT 0 COMMENT '购买数量',
  16. `bill_time` datetime DEFAULT NULL COMMENT '下单时间',
  17. PRIMARY KEY (`id`) USING BTREE
  18. ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='订单表';
  19. INSERT INTO `seckill`.`t_book`(`book_id`, `name`, `count`) VALUES (1, '《HaC的自传》', 5);

测试

1、配置Nginx

配置Nginx,分流进入两个服务。

修改nginx.conf

  1. upstream mysite {
  2. server 127.0.0.1:8090 weight=1;
  3. server 127.0.0.1:8091 weight=1;
  4. }
  5. server {
  6. listen 80;
  7. error_page 500 502 503 504 /50x.html;
  8. location = /50x.html {
  9. root html;
  10. }
  11. location / {
  12. proxy_pass http://mysite;
  13. }
  14. }

说明:当访问localhost:80 端口会分流到8090和8091端口

启动服务,启动两个端口的服务,模拟分布式部署。

(1)不加锁的情况下

使用jmeter 模拟并发。不加锁的情况模拟10个请求在1s发出 共2次,方便查看:

查看一下日志:

8090这台服务器:



8091这台服务器:



同一时间进入请求。

查询一下订单:



库存为0之后,但是初始化只有 5 本书,最后竟然出现了18个订单,显然是有问题的。

这就是不加锁的结果。

(2)加锁的情况下

8090服务器:



8091服务器:



看一下数据库:



刚好生成 5 个订单,没有超卖的现象。

redisson分布式锁的应用——秒杀、超卖 简单例子(分布式锁相关)的更多相关文章

  1. 【分布式锁的演化】“超卖场景”,MySQL分布式锁篇

    前言 之前的文章中通过电商场景中秒杀的例子和大家分享了单体架构中锁的使用方式,但是现在很多应用系统都是相当庞大的,很多应用系统都是微服务的架构体系,那么在这种跨jvm的场景下,我们又该如何去解决并发. ...

  2. mysql悲观锁处理赠品库存超卖的情况

    处理库存超卖的情况前,先了解下什么是乐观锁和悲观锁,下面的几篇博客已经介绍的比较详细了,我就不在赘述其原理了 [MySQL]悲观锁&乐观锁 对mysql乐观锁.悲观锁.共享锁.排它锁.行锁.表 ...

  3. 关于分布式锁原理的一些学习与思考-redis分布式锁,zookeeper分布式锁

    首先分布式锁和我们平常讲到的锁原理基本一样,目的就是确保,在多个线程并发时,只有一个线程在同一刻操作这个业务或者说方法.变量. 在一个进程中,也就是一个jvm 或者说应用中,我们很容易去处理控制,在j ...

  4. 【分布式锁】Redis实现可重入的分布式锁

    一.前言 之前写的一篇文章<细说分布式锁>介绍了分布式锁的三种实现方式,但是Redis实现分布式锁关于Lua脚本实现.自定义分布式锁注解以及需要注意的问题都没描述.本文就是详细说明如何利用 ...

  5. Springboot分别使用乐观锁和分布式锁(基于redisson)完成高并发防超卖

    原文 :https://blog.csdn.net/tianyaleixiaowu/article/details/90036180 乐观锁 乐观锁就是在修改时,带上version版本号.这样如果试图 ...

  6. 解决redis秒杀超卖的问题

    我们再使用redis做秒杀程序的时候,解决超卖问题,是重中之重.以下是一个思路. 用上述思路去做的话,我们再用户点击秒杀的时候,只需要检测,kucun_count中是否能pop出数据,如果能pop出来 ...

  7. 基于redis集群实现的分布式锁,可用于秒杀,定时器。

    在分布式系统中,经常会出现需要竞争同一资源的情况,使用redis可以实现分布式锁. 前提:redis集群已经整合项目,并且可以直接注入JedisCluster使用: @Autowired privat ...

  8. 【Redis 分布式锁】(1)一把简单的“锁”

    原文链接:https://www.changxuan.top/?p=1230 在单体架构向分布式集群架构演进的过程中,项目中必不可少的一个功能组件就是分布式锁.在开发团队有技术积累的情况下,做为团队的 ...

  9. zookeeper分布式锁,解决了羊群效应, 真正的zookeeper 分布式锁

    zookeeper 实现分布式锁,监听前一个节点来避免羊群效应, 思路:很简单,但是实现起来要麻烦一些, 而且我也是看了很多帖子,发现很多帖子的代码,下载下来逐步调试之后发现,看起来是对的,但在并发情 ...

  10. zookeeper 实现分布式锁zookeeper 使用 Curator 示例监听、分布式锁

    下载地址: http://download.csdn.net/download/ttyyadd/10239642

随机推荐

  1. odoo部署安全性问题

    本文档描述在生产中或在面向Internet的服务器上设置Odoo的基本步骤.它是在安装之后进行的,对于没有在internet上公开的开发系统来说,它通常不是必需的.警告如果您正在设置公共服务器,请务必 ...

  2. 原生AJAX的学习

    基础知识 知识点梳理见图: 自己动手实践案例 案例1: 访问本地文件 <!DOCTYPE html> <html> <body> <div id=" ...

  3. Hive执行计划之只有map阶段SQL性能分析和解读

    目录 目录 概述 1.不带函数操作的select-from-where型简单SQL 1.1执行示例 1.2 运行逻辑分析 1.3 伪代码解释 2.带普通函数和运行操作符的普通型SQL执行计划解读 2. ...

  4. 尚医通day13【预约挂号】(内附源码)

    页面预览 预约挂号 根据预约周期,展示可预约日期,根据有号.无号.约满等状态展示不同颜色,以示区分 可预约最后一个日期为即将放号日期 选择一个日期展示当天可预约列表 预约确认 第01章-预约挂号 接口 ...

  5. FPGA加速技术:如何提高系统的可编程性和灵活性

    目录 <23. FPGA加速技术:如何提高系统的可编程性和灵活性> 一.引言 随着人工智能.物联网等新技术的快速发展,对计算资源和处理能力的需求不断增加.为了加速计算流程和提高系统的性能, ...

  6. PostgreSQL 12 文档: 部分 VI. 参考

    部分 VI. 参考 这份参考中的条目意欲提供关于相应主题的权威.完整和正式的总结.关于使用PostgreSQL的更多信息(以叙述.教程或例子的形式)可以在本书的其他部分找到.见每个参考页面上列出的交叉 ...

  7. 解决Springboot项目打成jar包后获取resources目录下的文件失败的问题

    前几天在项目读取resources目录下的文件时碰到一个小坑,明明在本地是可以正常运行的,但是一发到测试环境就报错了,说找不到文件,报错信息是:class path resource [xxxx] c ...

  8. MySQL-this is incompatible with sql_mode=only_full_group_by 错误解决

    MySQL-this is incompatible with sql_mode=only_full_group_by 错误解决 编辑配置文件 Linux 中 :my.cnf Windows中 : m ...

  9. Spring 中 Bean 的配置细节

    前言 大家好,我是 god23bin,今天继续说 Spring 的内容,关于 Spring 中 Bean 的配置的,通过上一篇文章的学习,我们知道了 Spring 中的依赖注入,其中有两种主要的方式, ...

  10. Blazor前后端框架Known-V1.2.4

    V1.2.4 Known是基于C#和Blazor开发的前后端分离快速开发框架,开箱即用,跨平台,一处代码,多处运行. Gitee: https://gitee.com/known/Known Gith ...