分布式锁 : redisson、悲观乐 观锁
—、redisson
1. 与redistempplate区别:
2. 看门狗机制:
一、 简介
Redisson是一个Redis的基础上实现的Java驻内存数据网格,它不仅提供了一系列分布式的Java常用对象,还有一个重要的分布式锁的实现,主要作用为了防止分布式系统中的多个进程之间相互干扰。比如单机模式下的多线程用同步锁synchronized等解决数据一致性的并发操作,而分布式系统中则需要用redisson的lock或其他方式来解决。
二、 原理
底层其实就是基于分布式的Redis集群实现的。
用key作为是否上锁的标识,当通过getLock(String key)方法获得相应的锁后,这个key即作为一个锁存储到Redis集群中。 之后如果有其他的线程尝试获取名为key的锁时,便会向集群中进行查询,如果能够查到这个锁并发现相应的value的值不为0,则表示已经有其他线程申请了这个锁同时还没有释放,则当前线程进入阻塞,否则由当前线程获取这个锁并将value值加1。
三、 Watch Dog(看门狗机制)
作用就是:自动续期
- 解决指定解锁时间的重复解锁问题。(业务执行的时间超过指定时间,redis会自动解锁;当前业务执行完后又要解锁,可能会解锁到另一条线程加的锁或当前锁已失效)
2. 解决死锁。(加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认会在30s内自动过期)
四、 锁类型
根据不同业务场景需要redisson提供了多种锁的实现类型:可重入锁,公平锁,联锁,红锁,读写锁,信号量,可过期性信号量,闭锁。
这里看下最常用的可重入锁,特点是同一个线程可以重复拿到同一个资源的锁,非常有利于资源的高效利用。
底层实现:1. Redis存储锁的数据类型是Hash
- Hash数据类型的key值包含了当前线程信息
五、 redlock算法
底层的一个算法,可以了解一下。
当redis宕机时,即使有主从,但是依然会有一个同步间隔,如果造成数据流失,服务器A丢失锁,服务器B就可以获取锁,这样就造成数据错误。
redlock主要思想是做数据冗余。比如5台独立的集群,当我们发送一个数据的时候,要保证3台(n/2+1)以上的机器接受成功才算成功,否则重试或报错。
六、 项目配置
- 引入依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.0</version>
</dependency>
2.配置类RedissonClient
@Configuration
public class MyRedissonConfig {
/**
* 1.所有对redisson的使用都是通过RedissonClient来使用
* 2.支持4种连接redis方式,分别为单机、主从、Sentinel、Cluster集群 。此处为Sentinel模式
*/
@Bean(destroyMethod="shutdown")
public RedissonClient redisson() throws Exception{
//1 创建配置
Config config = new Config();
config.useSentinelServers()
.addSentinelAddress("redis://192.168.42.97:26379","redis://192.168.42.97:26380","redis://192.168.42.97:26381")
.setMasterName("mymaster")
.setDatabase(0);
//2.根据Config创建出RedissonClient
return Redisson.create(config);
}
}
3.操作示例
@Autowired
RedissonClient redissonClient;
@RequestMapping("test")
@ResponseBody
public void test() throws InterruptedException {
// getLock(key)方法 返回的是一个RedissonLock对象
RLock lock = redissonClient.getLock("my-lock");
// 1.redisson的自动续期,如果业务超长,运行期间自动续上30s,不用担心业务时间长,锁自动过期被删掉
// 2.加锁得业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认在30s后自动删除
lock.lock();
// 5秒以后自动解锁,自动解锁时间一定要大于业务时间,在锁时间到了以后,不会自动续期
//lock.lock(5, TimeUnit.SECONDS);
try {
System.out.println("线程"+Thread.currentThread().getId()+"加锁成功,执行业务1");
Thread.sleep(1000*8);
} catch (Exception e){
System.out.println(e);
} finally{
//解锁
System.out.println("线程"+Thread.currentThread().getId()+"释放锁1");
lock.unlock();
}
}
/**
* 可重入锁
*/
@RequestMapping("test2")
@ResponseBody
public void test2() throws InterruptedException {
RLock lock = redissonClient.getLock("my-lock");
lock.lock();
try {
System.out.println("线程"+Thread.currentThread().getId()+"加锁成功,执行业务2");
Thread.sleep(1000*8);
lock.lock();
System.out.println("线程"+Thread.currentThread().getId()+"加锁成功,执行业务2.2");
Thread.sleep(1000*8);
System.out.println("线程"+Thread.currentThread().getId()+"释放锁2.2");
lock.unlock();
} catch (Exception e){
System.out.println(e);
} finally{
System.out.println("线程"+Thread.currentThread().getId()+"释放锁2");
lock.unlock();
}
}
二、数据库锁:
Mysql 并发事务 会引起更新丢失问题,解决办法是锁。所以本文将对锁(乐观锁、悲观锁)进行分析。
1. 悲观锁:
1 概念(来自百科)
悲观锁,正如其名,指数据被 外界(包括当前系统的其它事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提 供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排它性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)
还可以理解,就是Java中的 Synchronized 关键字。只要对代码加了 Synchronized 关键字,JVM 底层就能保证其线程安全性。
2 命令行演示
2.1 准备数据
2.2 测试
测试准备:
- 两个会话(终端),左边会话是白色背景、右边会话是黑色背景
开始测试:
第一步:两个终端均关闭自动提交
左边:
右边:
第二步:左边利用 select .... for update 的悲观锁语法锁住记录
select * from employee where id = 1 for update;

第三步:右边也尝试利用 select .... for update 的悲观锁语法锁住记录
可以看到,Sql语句被挂起(被阻塞)!
提示:如果被阻塞的时间太长,会提示如下:
第四步:左边执行更新操作并提交事务
Sql语句:
update employee set money = 0 + 1 where id = 1; commit;
结果:
分析:
- Money 的旧值为0,所以更新时 Money=0+1
- 一执行 commit 后,注意查看右边Sql语句的变化
第五步:查看右边Sql语句的变化
分析:
- 被左边悲观锁阻塞了 11.33 秒
- Money=1,这是左边更新后的结果
2.3 结论
可以看到,当左边(事务A)使用了 select ... for update 的悲观锁后,右边(事务B)再想使用将被阻塞,同时,阻塞被解除后事务B能看到事务A对数据的修改,所以,这就可以很好地解决并发事务的更新丢失问题啦(诚然,这也是人家悲观锁的分内事)
2.乐观锁:
1 概念
理解方式一:
乐观锁认为一般情况下数据不会造成冲突,所以在数据进行提交更新时才会对数据的冲突与否进行检测。如果没有冲突那就OK;如果出现冲突了,则返回错误信息并让用户决定如何去做。
理解方式二:
乐观锁的特点是先进行业务操作,不到万不得已不会去拿锁。乐观地认为拿锁多半会是成功的,因此在完成业务操作需要实际更新数据的最后一步再去拿一下锁。
我的理解
理解一:就是 CAS 操作
理解二:类似于 SVN、GIt 这些版本管理系统,当修改了某个文件需要提交的时候,它会检查文件的当前版本是否与服务器上的一致,如果一致那就可以直接提交,如果不一致,那就必须先 更新服务器上的最新代码然后再提交(也就是先将这个文件的版本更新成和服务器一样的版本)
2 表设计
表task,分别有三个字段id,value、version
3 具体实现
-首先读取task表中的数据,得到version的值为versionValue
-在每次更新task表value字段时,因为要防止可能发生的冲突,我们需要这样操作select (value,version) from task where id=#{id}
update task
set value=newValue,version=versionValue+ 1
whereid=#{id} and version=versionValue;
只有当这条语句执行成功了,本次更新value字段的值才会表示成功。
我们假设有两个节点A与B都需要更新task表中的value字段值,在相同时刻,A和B节点从task表中读到的version值都为2,那么A节点和B节点在更新value字段值的时候,都需要操作
update task set value = newValue,version = 3 where version = 2;
实际上其实只有1个节点执行该SQL语句成功,我们假设A节点执行成功,那么此时task表的version字段的值是3,B节点再操作
update task
set value = newValue,version = 3
where version = 2;
这条SQL语句是不执行的,这样就保证了更新task表时不发生冲突
3.总结、对比
悲观锁 | 乐观锁 | |
概念 | 查询时 直接锁住记录 使其它事务不能查询,更不能更新 | 提交更新时 检查 版本或时间戳 是否相等 |
语法 | select ... for update | 使用 version 或者 timestamp 进行比较 |
实现者 | 数据库本身 | 开发者 |
适用场景 | 并发量大 | 并发量小 |
类比Java | Synchronized关键字 | CAS 算法 |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南