一、非分布式环境的锁使用:
说明:只能在非集群Web应用(单JVM环境)中使用。
1.update行锁,在...Mapper.xml中:
<mapper ...>
<update id="方法名">
update 表名 set ..., #行锁,update语句执行时,其他操作等待
where 条件
</update>
</mapper>
2.synchronized锁:
(1)说明:
特性:互斥性、可重入性、非公平性,由JVM实现(自动加锁/解锁),与ReentrantLock性能类似。
Mark Word:
状态 56bit 1bit 4bit 1bit偏向锁标志 2bit锁标志
无锁 1-25为空, 31位HashCode 空 对象年龄 0 01
偏向锁 54位线程ID, 2位epoch 空 对象年龄 1 01
轻量级锁 62位指针(指向轻量级锁记录) 00
重量级锁 62位指针(指向重量级锁记录) 10
GC标记 空 11
偏向锁:加/解锁耗时少,多线程夺锁时有锁撤销操作,所以适用单线程的业务场景
轻量级锁:锁占用时间短,多线程夺锁不阻塞,夺不到锁时会自适应自旋浪费CPU资源,所以适用耗时少的业务场景
重量级锁:多线程夺锁不会浪费CPU资源,夺不到锁时会阻塞,所以适用耗时长的业务场景
锁升级流程:
检查锁状态 -> 1.偏向锁、无锁(无锁时跳第3步) -> 2检查线程ID指向A线程(是时执行同步代码,结束流程) -> 3 CAS替换线程ID(成功时执行同步代码,结束流程) -> 4撤销A线程偏向锁 -> 5暂停其他线程并检查状态(非活动状态时,释放其他线程,唤醒A线程,A线程跳第3步) -> 6 A线程升到轻量级锁 -> 6.5 CAS操作(成功时执行同步代码,结束流程) -> 7 A线程自适应自旋(获得轻量锁时执行同步代码,结束流程) -> 8 A线程升到重量级锁 -> 9 A线程阻塞等待其他线程释放锁
(2)使用:
synchronized方法锁:
@Autowired
PlatformTransactionManager platformTransactionManager;
@Autowired TransactionDefinition transactionDefinition;
public String synchronized createServiceOrder(){ //1.方法添加synchronized
//2.获取事务
TransactionStatus t = platformTransactionManager.getTransaction(transactionDefinition);
... //查询医院与医生信息
if(判断当前医院与医生是否存在){//3.1不存在时回滚
platformTransactionManager.rollback(t);
throw new Exception("选择的医院或医生不存在");
}
if(判断当前用户剩余服务次数){//3.2次数为0时回滚
platformTransactionManager.rollback(t);
throw new Exception("选择的医院或医生不存在");
}
... //更新数据:减少用户的服务次数
//4.提交事务
platformTransactionManager.commit(t);
... //5.生成订单
return 订单id;
}
synchronized块锁:
@Autowired
PlatformTransactionManager platformTransactionManager;
@Autowired TransactionDefinition transactionDefinition;
public String createServiceOrder(){
synchronized(){ //1.添加synchronized
//2.获取事务
TransactionStatus t = platformTransactionManager.getTransaction(transactionDefinition);
... //查询医院与医生信息
if(判断当前医院与医生是否存在){//3.1不存在时回滚
platformTransactionManager.rollback(t);
throw new Exception("选择的医院或医生不存在");
}
if(判断当前用户剩余服务次数){//3.2次数为0时回滚
platformTransactionManager.rollback(t);
throw new Exception("选择的医院或医生不存在");
}
... //更新数据:减少用户的服务次数
//4.提交事务
platformTransactionManager.commit(t);
}
... //5.生成订单
return 订单id;
}
3.ReentrantLock锁(可重入锁):
特性:互斥性、可重入性、非公平(性能高)/公平性,由JDK实现(手动加锁/解锁),与synchronized性能类似。
(1)ReentrantLock使用:
Lock lock = new ReentrantLock(); //非公平性
//Lock lock = new ReentrantLock(true); //公平性
@Autowired
PlatformTransactionManager platformTransactionManager;
@Autowired TransactionDefinition transactionDefinition;
public String createServiceOrder(String id){
//1.获取锁
lock.lock();
//lock.tryLock(); //尝试获得锁,失败返回false
//lock.tryLock(时间, 单位); //同上,指定超时时间
//lock.lockInterruptibly(); //获得锁,线程Interrupt时抛出异常
try {
//2.获取事务
TransactionStatus t = platformTransactionManager.getTransaction(transactionDefinition);
... //查询医院与医生信息
if(判断当前医院与医生是否存在){//3.1不存在时回滚
platformTransactionManager.rollback(t);
throw new Exception("选择的医院或医生不存在");
}
if(判断当前用户剩余服务次数){//3.2次数为0时回滚
platformTransactionManager.rollback(t);
throw new Exception("选择的医院或医生不存在");
}
... //更新数据:减少用户的服务次数
//4.提交事务
platformTransactionManager.commit(t);
} finally {
//5.释放锁
lock.unlock();
}
... //6.生成订单
return 订单id;
}
(2)Condition暂停/唤醒线程(写在lock - unlock中间):
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition(); //获得Condition
public void wait1(){
try {
lock.lock();
//...
condition.await(); //暂停线程,写在lock - unlock中间,等同Object.wait()
} finally {
lock.unlock();
}
}
public void notify1(){
try {
lock.lock();
//...
condition.signal(); //唤醒线程,写在lock - unlock中间,等同Object.notify()
//condition.signalAll(); //唤醒线程,写在lock - unlock中间,等同Object.notifyAll()
} finally {
lock.unlock();
}
}
4.ReentrantReadWriteLock锁:
特性:线程间读锁共享/写锁互斥性、可重入性、非公平(性能高)/公平性、锁降级(从写锁降级为读锁),由JDK实现(手动加锁/解锁),适用读多写少的场景。
获取读锁:获取读锁时,代码块不能有其他线程获得写锁。
获取写锁:代码块不能有其他线程的读锁与写锁。
(1)使用:
lock.readLock().lock(); //1.获取读锁
if (true) { //判断缓存中数据是否失效,失效进入if内部
//1.1释放读锁,获得写锁,更新缓存数据
lock.readLock().unlock();
lock.writeLock().lock();
//...1.2向缓存中写新数据
//1.3获取读锁,释放写锁 -> 锁降级
lock.readLock().lock();
lock.writeLock().unlock();
}
//...从缓存中获取数据
lock.readLock().unlock(); //2.释放读锁
5.StampedLock锁(ReentrantReadWriteLock加强版):
特性:不可重入、Writing/Reading/Optimistic reading三种模式、允许读锁/写锁互转、获得锁时next=0表示失败,也适用读多写少的场景。
Writing模式:类似ReentrantReadWriteLock写锁
Reading模式(悲观读):类似ReentrantReadWriteLock读锁,读过程中不允许写操作
Optimistic reading模式(乐观读):读过程中允许写操作
(1)写锁使用:
private void insert() {
long next = 0;
try {
next = lock.writeLock(); //获得写锁
//...写业务
} finally {
lock.unlockWrite(next); //释放写锁
}
}
(2)乐观读锁+悲观读锁使用:
private String query(){
long next = lock.tryOptimisticRead(); //1.1 获得乐观读锁
if(true){ //验证是否有写操作,有进入if
try {
next = lock.readLock(); //1.2 获得悲观读锁,重新读数据
//...读业务
} finally {
lock.unlockRead(next); //2 释放悲观读锁
}
}
return "...";
}
(3)悲观读锁+写锁使用:
private String queryAndInsert() {
long next = lock.readLock(); //1.1 获得悲观读锁
//...读业务
try {
long tempNext = lock.tryConvertToWriteLock(next); //1.2 将悲观读锁转为写锁
if (tempNext != 0) {
next = tempNext; //获得写锁成功
} else { //1.3切换写锁失败时,关闭悲观读锁,获取写锁
lock.unlockRead(next);
next = lock.writeLock();
}
//...写业务
} finally {
lock.unlock(next); //2 释放锁
}
return "...";
}
二、分布式环境的锁使用:
说明:能在集群Web应用(多JVM环境)中使用。
1.数据库悲观锁(对数据库压力大):
格式(操作同一条数据,使用for update锁定,其他线/进程进入等待):
select * from 表名 where 条件 for update;
mapper映射文件:
UserMapper.xml内容(sql语句后加for update):
<mapper ...>
<select id="queryUser" resultType="com.yyh.domain.User">
select * from user
where user_id = #{userId,jdbcType=VARCHAR}
for update
</select>
</mapper>
Dao类:
public class UserMapper {
...
User queryUser(@Param("userId")String userId);
}
Service类:
@Resource
UserMapper userMapper;
@Transactional(rollbackFor = Exception.class) //对方法加事务
public User getUser(String userId){
return userMapper.queryUser(userId);
}
2.Redis分布式锁(不支持阻塞):
说明:
随机值:用于自动释放锁时校验
NX:原子性,利用此值获取锁,键存在时不设值,键不存在时设值并获得锁
PX:过期时间,单位为毫秒,过期自动释放锁
SET 键名 随机值 NX PX 毫秒值
(1)获取/释放Redis锁:
public class RedisDisLock implements AutoCloseable { //继承
RedisTemplate redisTemplate;
String key; //键名
String randomValue; //随机值
public RedisDisLock(RedisTemplate redisTemplate, String key){
this.redisTemplate = redisTemplate;
this.key = key; //键名
this.randomValue = UUID.randomUUID().toString(); //随机值
}
//获得Redis锁
public boolean getRedisLock(){
RedisCallback<Boolean> callback = connection -> {
byte[] keyB = redisTemplate.getKeySerializer().serialize(key); //将键名转为byte[]
byte[] randomValueB = redisTemplate.getKeySerializer().serialize(randomValue); //将随机值转为byte[]
Expiration expiration = Expiration.seconds(60); //过期时间
RedisStringCommands.SetOption nx = RedisStringCommands.SetOption.ifAbsent(); //NX
return connection.set(keyB, randomValueB, expiration, nx);
}
return redisTemplate.execute(callback); 获取Redis锁
}
//释放锁(Lua脚本):
public boolean unRedisLock(){
RedisScript<Boolean> script = RedisScript.of(
"if redis.call(\"get\", KEYS[1]) == ARGV[1] then\n" + //校验锁
" return redis.call(\"del\", KEYS[1])\n" + //释放锁
"else\n" +
" return 0\n" +
"end",
Boolean.class);
return (Boolean) redisTemplate.execute(script, Arrays.asList(key), randomValue); //释放Redis锁
}
}
//自动释放锁
@Override
public void close() throws Exception {
unRedisLock(); //释放锁
}
}
(2)使用Redis锁:
@Autowired
RedisTemplate redisTemplate;
public void test(){
try (RedisDisLock lock = new RedisDisLock(redisTemplate, "自定义键名")) {
if (lock.getRedisLock()){ //获取锁,true表示有锁,false为没有锁
//此处执行锁内具体逻辑
}
} catch (Exception e) {
e.printStackTrace();
}
}
3.Redisson分布式锁(基于Redis实现,支持阻塞):
官方文档:
https://github.com/redisson/redisson/wiki/2.-%E9%85%8D%E7%BD%AE%E6%96%B9%E6%B3%95
(1)使用Java代码配置方式,获取Redisson锁:
<1>导入Redisson依赖包,在pom.xml中:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.11.2</version>
</dependency>
<2>单点Redis:
public void test(){
Config config = new Config();
config.setTransportMode(TransportMode.EPOLL);
config.useSingleServer() //单点Redis
.addNodeAddress("redis://192.168.133.141:7181"); //可以用"rediss://"来启用SSL连接
RedissonClient redisson = Redisson.create(config);
RLock lock = redisson.getLock("自定义键名");
try{
lock.lock(30, TimeUnit.SECONDS); //获得锁
//此处执行锁内具体逻辑
} catch (Exception e){
e.printStackTrace();
} finally {
try{
lock.unlock(); //释放锁
} catch (Exception e){
e.printStackTrace();
}
}
}
<3>集群Redis:
public void test(){
Config config = new Config();
config.useClusterServers() //集群Redis
.setScanInterval(2000) // 集群状态扫描间隔时间,单位是毫秒
.addNodeAddress("redis://192.168.233.133:6379", "redis://192.168.233.134:6379")
.addNodeAddress("redis://192.168.233.135:6379") //可以用"rediss://"来启用SSL连接
RedissonClient redisson = Redisson.create(config);
//后面代码同上单点Redis方式
}
(2)使用SpringBoot配置方式,获取Redisson锁:
<1>导入依赖:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.11.2</version>
</dependency>
<2>application.yml文件中配置redis:
spring:
redis:
database: 0
host: 192.168.233.133 #redis服务器IP
port: 6379 #redis服务器端口
... #其他redis的配置
<3>代码:
@Autowired
RedissonClient redisson; //自动装载
public void test(){
RLock lock = redisson.getLock("自定义键名");
try{
lock.lock(30, TimeUnit.SECONDS); //获得锁
//此处执行锁内具体逻辑
} catch (Exception e){
e.printStackTrace();
} finally {
try{
lock.unlock(); //释放锁
} catch (Exception e){
e.printStackTrace();
}
}
}
4.Zookeeper分布式锁(支持阻塞):
(1)进入Zookeeper命令行:
#查看节点列表
ls /
#创建分布式锁节点
create /lock dis-lock
#获取指定节点数据
get /lock
(2)获取/释放ZooKeeper锁:
public class ZooKeeperLock implements AutoCloseable, Watcher {
ZooKeeper zooKeeper;
String tempNode;
public ZooKeeperLock(){
//第1个参数为ZooKeeper服务器的IP+端口,第2个参数为超时时间
this.zooKeeper = new ZooKeeper("192.168.233.141:2181", 10000, this);
}
public boolean getZKLock(String name){ //获取锁,name为业务名称(自定义)
try{
Stat s = zookeeper.exists("/" + name, false); //判断根节点是否存在
if(s == null){ //根节点不存在时
zooKeeper.create("/" + name, name.getBytes() ZooDefs.lds.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); //1.创建根节点
}
tempNode = zooKeeper.create("/" + name + "/" + name + "_", name.getBytes() ZooDefs.lds.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL); //2.创建瞬时有序节点
List<String> childNodeList = zooKeeper.getChildren("/" + name, false); //获取子节点列表
Collections.sort(childNodeList); //对子节点列表排序
if(tempNode.endsWith(childNodeList.get(0))) return true; //判断创建的节点是否是第1个,是获得锁,不是进入下面监听前一个节点
/*
3.监听上一个节点
*/
String preNode = childNodeList.get(0);
for (String itemNode: childNodeList) {
if(tempNode.endsWith(itemNode)){
zookeeper.exists("/" + name + "/" + preNode, true);
break;
}
preNode = itemNode;
}
synchronized (this) {
wait(); //进入等待
}
return true;
} catch (Exception e){
e.printStackTrace();
}
}
@Override
public void close() throws Exception { //自动释放锁
zookeeper.delete(tempNode, -1); //删除节点
zookeeper.close(); //关闭
}
@Override
public void process(WatchedEvent e) {
if(e.getType() == Event.EventType.NodeDeleted){
synchronized(this){
notify(); //取消等待
}
}
}
}
(3)使用ZooKeeper锁:
public void test(){
try (ZooKeeperLock lock = new ZooKeeperLock()) {
if (lock.getZKLock("业务名称")){ //获取锁,true表示有锁,false为没有锁
//此处执行锁内具体逻辑
}
} catch (Exception e) {
e.printStackTrace();
}
}
5.Curator分布式锁(依赖ZooKeeper):
(1)导入Curator依赖包,在pom.xml中:
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.2.0</version>
</dependency>
(2)使用Curator锁:
Application类中:
//启动类中实例化CuratorFramework
@StringBootApplication
public class MyApplication{
... //其他代码
@Bean(initMethod = "start", destroyMethod = "close") //初始化时调用CuratorFramework的start()方法,结束时调用CuratorFramework的close()方法
public CuratorFramework getCuratorFramework(){ //实例化CuratorFramework
RetryPolicy rp = new ExponentialBackoffRetry(1000, 3)
return CuratorFrameworkFactory.newClient("192.168.233.141:2181", rp); //连接ZooKeeper服务器IP与端口,返回CuratorFramework客户端对象
}
}
在需要的地方使用锁:
@Autowired
CuratorFramework client;
public void test(){
InterProcessMutex lock = new InterProcessMutex(client, "/name"); //name为业务名称
try{
if(lock.acquire(30, TimeUnit.SECONDS)){ //获取锁
//此处执行锁内具体逻辑
}
} catch (Exception e){
e.printStackTrace();
} finally {
try{
lock.release(); //释放锁
} catch (Exception e){
e.printStackTrace();
}
}
}