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实现分布式锁的核心就是

加锁

SET key value NX EX timeOut

参数说明:

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

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

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

代码实现:

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

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

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

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

解锁:

代码实现:

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

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

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

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

加锁流程



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

if (redis.call('exists' KEYS[1]) == 0) then  +  --  exists 判断key是否存在
redis.call('hset' KEYS[1] ARGV[2] 1); + --如果不存在,hset存哈希表
redis.call('pexpire' KEYS[1] ARGV[1]); + --设置过期时间
return nil; + -- 返回null 就是加锁成功
end; +
if (redis.call('hexists' KEYS[1] ARGV[2]) == 1) then + -- 如果key存在,查看哈希表中是否存在(当前线程)
redis.call('hincrby' KEYS[1] ARGV[2] 1); + -- 给哈希中的key加1,代表重入1次,以此类推
redis.call('pexpire' KEYS[1] ARGV[1]); + -- 重设过期时间
return nil; +
end; +
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那就会自动失效了呀~)

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

config.setLockWatchdogTimeout(30000);

看门狗有什么用呢?

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

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

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

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

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

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

释放锁机制

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

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

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

可能存在的问题

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

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

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

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

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

引入依赖

       <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.7.3</version>
</dependency>

yml文件redis配置

spring:
redis:
database: 0
host: 127.0.0.1
port: 6124

配置redisson

@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
// config.useSingleServer();//单机
// config.useMasterSlaveServers();//集群
// config.useSentinelServers();//哨兵
// config.useClusterServers();//集群
// config.setLockWatchdogTimeout(30000);
//使用的Redis主从模式
config.useMasterSlaveServers()
.setPassword("redis")
.setMasterAddress("redis://82.71.16.139:6379")
.addSlaveAddress("redis://82.71.16.139:6380","redis://82.71.16.139:6381");
return Redisson.create(config);
}

新建两个实体

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

这里模拟了两种情况:

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

@Slf4j
@Service
public class BookOrderService { @Autowired
BookMapper bookMapper; @Autowired
OrderMapper orderMapper; @Autowired
RedissonClient redissonClient; public String seckill(Long bookId, Long userId) {
return notLockDemo(bookId, userId);
// return lockDemo(bookId, userId);
} String lockDemo(Long bookId, Long userId) {
final String lockKey = bookId + ":" + "seckill" + ":RedissonLock";
RLock rLock = redissonClient.getLock(lockKey); try {
// 尝试加锁,最多等待20秒,上锁以后10秒自动解锁
Boolean flag = rLock.tryLock(20, 10, TimeUnit.SECONDS); if (flag) {
//1、判断这个用户id 是否已经秒杀过
List<Order> list = orderMapper.selectList(new QueryWrapper<Order>().lambda().eq(Order::getUserId, userId).eq(Order::getStatus, 1).eq(Order::getBookId, bookId));
if (list.size() >= 1) {
log.info("你已经抢过了");
return "你已经抢过了,一人只能抢一次";
} //2、查库存
Book book = bookMapper.selectOne(new QueryWrapper<Book>().lambda().eq(Book::getBookId, bookId));
if (book != null && book.getCount() > 0) {
//生成订单
String orderId = UUID.randomUUID().toString();
Order newOrder = Order.builder().
orderId(orderId).
status(1).
bookId(bookId).
userId(userId).
count(1).
billTime(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())).build(); orderMapper.insert(newOrder); //更新库存
Book newBook = Book.builder().count(book.getCount() - 1).build();
bookMapper.update(newBook, new QueryWrapper<Book>().lambda().eq(Book::getBookId, bookId));
log.info("userId:{} 秒杀成功", userId);
return "秒杀成功" + "";
} else {
log.info("秒杀失败,被抢完了");
}
} else {
log.info("请勿重复点击,userid:{} ", userId);
return "你已经抢过了";
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (rLock.isLocked()) {
if (rLock.isHeldByCurrentThread()) {
rLock.unlock();
}
}
}
return "很遗憾,没货了...";
} String notLockDemo(Long bookId, Long userId) {
//1、判断这个用户id 是否已经秒杀过
List<Order> list = orderMapper.selectList(new QueryWrapper<Order>().lambda().eq(Order::getUserId, userId).eq(Order::getStatus, 1).eq(Order::getBookId, bookId));
if (list.size() >= 1) {
log.info("你已经抢过了");
return "你已经抢过了,一人只能抢一次";
} //2、查库存
Book book = bookMapper.selectOne(new QueryWrapper<Book>().lambda().eq(Book::getBookId, bookId));
if (book != null && book.getCount() > 0) {
//生成订单
String orderId = UUID.randomUUID().toString();
Order newOrder = Order.builder().
orderId(orderId).
status(1).
bookId(bookId).
userId(userId).
count(1).
billTime(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())).build();
orderMapper.insert(newOrder);
//更新库存
Book newBook = Book.builder().count(book.getCount() - 1).build();
bookMapper.update(newBook, new QueryWrapper<Book>().lambda().eq(Book::getBookId, bookId));
log.info("userId:{} 秒杀成功", userId);
return "秒杀成功" + "";
} else {
log.info("秒杀失败,被抢完了");
return "很遗憾,没货了...";
}
}
}

新建两个表

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

测试

1、配置Nginx

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

修改nginx.conf

	upstream mysite {
server 127.0.0.1:8090 weight=1;
server 127.0.0.1:8091 weight=1;
}
server {
listen 80; error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
} location / {
proxy_pass http://mysite;
}
}

说明:当访问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. 图解Redis和Zookeeper分布式锁

    1.基于Redis实现分布式锁 Redis分布式锁原理如上图所示,当有多个Set命令发送到Redis时,Redis会串行处理,最终只有一个Set命令执行成功,从而只有一个线程加锁成功 2:SetNx命 ...

  2. VS code 的安装

    VS code 的安装 Win10环境配置(一)--C\C++篇 Win10环境配置(二) --Java篇 安装前先 ,完成环境的配置 1.工具准备 官网下载:Visual Studio Code 2 ...

  3. 让你的代码动起来:Python进度条神器tqdm详解及应用实例

    各位Python高手,今天我要给大家介绍一个好用的库,它就是:tqdm tqdm在阿拉伯语中的意思是 "进展",所以这个库也被称为 "快速进展条".不得不说,这 ...

  4. C2简介

    C2简介 学习命令与控制的基本知识,帮助您成为更好的红队队员并简化您的下一次红队评估! thm:https://tryhackme.com/room/introtoc2 介绍 命令与控制 ( C2 ) ...

  5. Vulhub靶场的搭建

    Vulhub靶场的搭建(基于centos7) 1>简述 很多人在搭建Vulhub靶场的时候,可能也搜到过许多的文章,但是大多数的文章只是有一个流程,对其中的原理,步骤没有进行详细的说明,这也就导 ...

  6. ABP - 本地事件总线

    1. 事件总线 在我们的一个应用中,经常会出现一个逻辑执行之后要跟随执行另一个逻辑的情况,例如一个用户创建了后续还需要发送邮件进行通知,或者需要初始化相应的权限等.面对这样的情况,我们当然可以顺序进行 ...

  7. win10系统网络图标变成一个地球模型并且无法连上网络

    最近在家远程办公,但是遇到个很棘手的问题,电脑突然连不上无线网络了.... 无线网络图标变成地球模型如下:

  8. Unity 4.6 bate 20 or 4.5.5 +vuforia3.0.9 发布到真机错误 解决

    错误图 +错误码 014-11-20 15:45:49.224 youzheng[6527:1035587] ################### enable 32014-11-20 15:45: ...

  9. CF1799B Equalize by Divide题解

    本蒟蒻学习了jiangly大佬的思想,来发一个题解. 大致题意: 给定一个 \(n\) 个元素的数组 \(a\),每次可以选择 \(a[i]\) 和 \(a[j]\),然后使 \(a[i] = \lc ...

  10. 【Kubernetes】yaml文件编写 -- 持续更新

    K8S通过yaml格式的声明式API与资源对象交互 API版本由apiVersion字段指定,API对象类型由kind字段指定 除此之外,每个API对象有三大类属性: metadata:元数据 spe ...