在单体架构中,我们可以通过sychronized来保证并发安全,但是在分布式架构中,sychronized没用,sychronized只能保证一个进程(JVM)中的线程安全,无法跨进程
最简单的redis实现就是通过 redis的setnx(set if not exists)命令来实现
RedisTemplate对redis进行了封装,使用redistemplate就能实现 redis的setnx(set if not exists)命令
bool result = stringRedisTemplate.opsForValue().setIfAbsent( "key","value" ); // setIfAbsent方法就相当于 setnx命令
setIfAbsent方法 返回 true 说明key不存在,插入数据成功
setIfAbsent方法 返回 true 说明key已经存在了,插入数据失败
大概的代码逻辑就是
bool result = stringRedisTemplate.opsForValue().setIfAbsent( "key","value" ); // 设置key(相当于竞争锁 )
if(!result){
return "key已经存在";
}
执行业务逻辑代码(比如扣库存)
stringRedisTemplate.delete("key"); // 删除key (相当于释放锁)
想想这里面可能出现的问题:
1、线程在运行业务代码的时候出现异常,那么此时就无法释放锁,就会形成死锁。怎么办? =====》try finally ,在finally中执行delete操作
2、线程在执行业务代码的时候,服务器宕机了,此时也无法释放锁,也会形成死锁。怎么办? =====》 给 锁的key设置一个timeout
int timeout = 10;
stringRedisTemplate.expire("key",timeout ,TimeUnit.SECONDS);
3、此时加锁与设置timeout之间是有时间损耗的,也就是说如果线程在加完锁正准备设置timeout时,此时服务宕机了,timeout就设置不了,就跟上面情况类似了,也会产生死锁,怎么办? ================》 保证 加锁与设置timeout 的原子性 ( RedisTemplate 提供了一个重载的setIfAbsent方法来保证原则性,此方法底层就是使用了lua脚本)
redis会将lua脚本中所有的命令当作一个原子操作执行
int timeout = 10;
// bool result = stringRedisTemplate.opsForValue().setIfAbsent( "key","value" );
// stringRedisTemplate.expire("key",timeout ,TimeUnit.SECONDS);
bool result = stringRedisTemplate.opsForValue().setIfAbsent( "key","value" ,timeout ,TimeUnit.SECONDS); // 这行代码就相当于把上面两行代码进行了原子性操作
4、线程 1 还没执行完业务代码,此时锁timeout了,redis把线程1 加的锁删了 ,此时线程 2 过来了加了锁,然后执行业务代码,注意! 此时线程 1 和线程 2 可能同时操作共享资源 ,然后线程1 此时执行完了删锁,注意! 此时线程 1 删除的锁是线程 2 加的锁,然后此时线程 3过来了加了锁,然后执行业务代码 ,注意! 此时线程 2 和线程 3可能同时操作共享资源,然后线程2 此时执行完了删锁,注意! 此时线程2 删除的锁是线程 3 加的锁,......往复循环 ,导致锁永久失效。
怎么办?=====》给自己加的锁 加一个标识,这个标识只有自己知道,此时锁就只能自己释放,别人就释放不了了
5、线程 1 还没执行完业务代码,此时锁timeout了,redis把线程1 加的锁删了 ,此时线程 2 过来了加了锁,然后执行业务代码,注意! 此时线程 1 和线程 2 可能同时操作共享资源 ,然后线程1 此时执行完了业务代码之后删锁,哦吼,我的锁怎么没了,然后就报错了。怎么办?
======》加大timeout的值
=======》但是有个问题,timeout越大 ====== 》 服务宕机之后,死锁时间越长 =======》其他线程等待的时间越长 ======》用户的脾气越大。 怎么办?
======》开启一个异步线程,开启一个定时任务,每个 1/3 timeout扫描一次,看看当前锁的key 还在不在,当前线程还在不在,是不是死(宕机 )了,然后线程还在,锁的key也还在,那么就给锁续命,重新设置锁的key 的 timeout ,只要线程没执行完就给锁的key续命,知道线程执行完自动删除锁的key(释放锁)
此时就不得不介绍一个神奇的框架了 !!!!redisson框架
redisson框架 ---- 一个 redis java client
redisson框架 中解决了上面,底层就是用的setnx ,lua脚本保证原子性,锁的key默认timeout 30s ,其他线程自旋 , 用timerTask 定时任务 默认 每隔 1/3 timeout 续命一次。
redisson分布式锁实现原理:
redisson框架 使用如下:
6、如果redis用了集群(一主多从),线程 1 的锁的key刚把锁的key存入redis中, redis的master 主节点挂了,锁的key还没同步到slave从节点,然后此时slave从节点升为 master主节点,但是此时这个新的master主节点中没有线程 1 的锁的key ,但是此时线程 1 还在执行业务代码,然后才是 线程 2 来了,由于此时新的master主节点上没有锁的key,所以线程2申请锁成功,然后此时线程 1 和 线程 2 就可能同时访问并使用共享资源 。 此时就有问题了呀! 怎么办?
=======》①用zookeeper 有延时 ,要同步半数以上的follower才能加锁成功,所以此时不用担心leader挂了,follower中没有锁的key ②人工补偿 ③redis自己解决 用redlock
redlock 实现原理
大概就是搞多个对等的redis节点,节点之间没有依赖(主从依赖),通过 setnx命令 加锁 ,同时发给每个redis节点,发送的这些节点中要有半数以上的redis节点加锁成功,才算认为线程拿到了锁,才继续往下执行业务。(原理跟zookeeper类似 , 牺牲了性能,如果没超过半数,涉及到锁回滚问题)
7、高并发性下请求到了redis,但是redis是单线程工作模式,在redis中就也不是并发执行,而是串行执行,影响性能。怎么办?
====》 用库存举例, 将库存分段存储,分段加锁=======》把每段的锁的key放到redis集群中,把不同锁的key放到不同的redis master主节点上
一个段的库存不够减怎么办?去减下面段的,合并几个段一起扣减。
由于zookeeper上面的临时节点是唯一的,只会创建一个,而锁也只有一把,所以它能代表这个锁,多个客户端去抢这个锁,也就是去zookeeper上面创建临时节点,看谁创建的快,谁第一个创建了这个临时节点谁就获取到了锁。
此时clientA第一个成功创建了临时节点,就代表clientA已经获取到了锁,其他client再去创建这个临时节点,发现这个节点已经存在了,他们就不能去创建了,就都阻塞了,他们一直监听这个节点的变化,也就是在监听这个锁的变化(通过zookeeper Watch功能)
clientA完成了它的业务操作 ,结束了与zookeeper的会话 临时节点被自动删除,就代表释放了锁,此时其他client监听到了临时节点被删除了(锁释放了),就都会去竞争锁,也就是去创建临时节点。
同一时间有多个客户端在竞争锁
======》上面这个实现方式有什么问题呢 ?想想如果上千个client去监听这个临时节点的变化,一旦这个临时节点变化了,然后此时就是上千个client去竞争这个锁(羊群效应 / 惊群效应),这对于zookeeper的压力是非常大的,所以这个方案不可行。那怎么办呢?
======》使用临时顺序节点 + 监听
使用临时顺序节点创建出来的节点都是有序的,只有最小的节点能拿到锁,其他的节点监听比它小的前一个节点。
获取锁大概流程: 每个client 分别 创建临时顺序节点,然后谁的节点最小就谁能拿到锁,其他节点就监听比自己小的前一个节点
比如下图:A B C D分别创建节点,A最先创建,所以它的节点编号最小,所以它能获取锁,然后B监听A,C监听B,D监听C
释放锁大概流程:clientA 执行完自己的业务之后结束了与zookeeper的会话,会话一结束,节点会自动删除(释放了锁),同时clientB监听到了clientA的这个节点被删除后,会去判断自己的节点是不是最小的节点,如果是最小的就获取锁,如果不是就继续监听等待,不做任务处理,clientA节点删除只对clientB有影响,对后面的 C D 等节点没有影响,因为C只监听B,B是没有变化的,所以C不会受到影响,不会变,D只监听C,C没有变,D也不会有影响,也不会变.......就解决的羊群(惊群)效应。
同一时间只有一个客户端在竞争锁
大概流程: 自己看图 , 直接通过 curator框架就可以实现, curator已经将这些问题解决了,封装好了,直接用就行。
基于数据库表(主要原理:数据库的主键不能重复,作为分布式锁的实现)
0、首先先创建一个tb_lock表,用于记录当前哪个线程正在使用数据,表里就一个 业务ID 或者业务名称 字段 (唯一主键),当作锁
1、当线程要访问数据时,会先将要执行的业务的业务id或者业务名称 ,insert插入tb_lock表中
2、当插入成功,代表该线程获得了锁,即可执行业务逻辑
3、当其他线程在执行相同业务的时候也会先将要执行业务的 业务id或者业务名称 插入tb_lock表中,由于主键冲突,此时会导致插入失败,就代表获取锁失败
4、获取锁成功的线程在执行完业务代码后,在删除tb_lock表中删除对应业务的 业务id或者业务名称 ,代表释放锁
想想这样实现会出现问题吗?
1、一旦数据库挂掉,会导致业务系统不可用。
====》用两个数据库,一主一从,数据之前双向同步。一旦挂掉快速切换到备库上。
2、这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。
====》做一个异步定时任务,每隔一定时间把数据库中的超时数据清理一遍。
3、这把锁只能是非阻塞的,因为数据的insert操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作。
====》用while循环,直到insert成功再返回成功。
4、这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。
====》在数据库表中加个字段,记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了。
基于数据库表的排它锁实现(基于MySql的InnoDB引擎)
1、在执行业务逻辑之前,先通过一个带有for update 的查询语句 拿到排它锁
select * from table where productId = 1 for update
行锁 : 当前连接要执行的带有 for update 的SQL语句以后,指定了主键查询,代表当前连接锁定了这条数据(productId = 1)
select * from table for update
表锁:当前连接执行带有for update的SQL语句以后,没有指定主键查询,那么会将表进行锁定,只有当前连接可以对这张表进行操作
2、获得排它锁的线程即可获得分布式锁,执行业务逻辑
3、执行完业务逻辑之后,再通过JDBC的connection.commit() 方法来释放锁
想想这样实现会出现问题吗?
1、一旦数据库挂掉,会导致业务系统不可用。
====》使用这种方式,此问题不会发生,服务宕机之后数据库会自己把锁释放掉。
2、这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。
====》做一个异步定时任务,每隔一定时间把数据库中的超时数据清理一遍。
3、这把锁只能是非阻塞的,因为数据的insert操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作。
====》使用这种方式,此问题不会发生,for update
语句会在执行成功后立即返回,在执行失败时一直处于阻塞状态,直到成功。
4、这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。
====》在数据库表中加个字段,记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了。