分布式锁的应用场景与分布式锁实现(三):基于Zookeeper实现分布式锁
基于MySQL实现分布式锁
不管是JVM锁还是MySQL锁,为了保证线程的并发安全,都提供了悲观独占排他锁。所以独占排他也是分布式锁的基本要求。
可以利用唯一键索引不能重复插入的特点表现,设计表如下:
CREATE TABLE `db_lock` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`lock_name` varchar(50) NOT NULL COMMENT '锁名',
`class_name` varchar(100) DEFAULT NULL COMMENT '类名',
`method_name` varchar(50) DEFAULT NULL COMMENT '方法名',
`server_name` varchar(50) DEFAULT NULL COMMENT '服务器ip',
`thread_name` varchar(50) DEFAULT NULL COMMENT '线程名',
`create_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '获取锁时间',
`desc` varchar(100) DEFAULT NULL COMMENT '描述',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_unique` (`lock_name`)
) ENGINE=InnoDB AUTO_INCREMENT=1332899824461455363 DEFAULT CHARSET=utf8;
基本思路
synchronized关键字和ReentrantLock锁都是独占排他锁,即多个线程争抢一个资源时,同一时刻只有一个线程可以抢占该资源,其他线程只能阻塞等待,知道占有资源的线程释放该资源。
- 1、线程同时获取锁(insert)
- 2、获取成功,执行业务逻辑,执行完成释放锁(delete)
- 3、其他线程等待重试
代码实现
新建数据库实体类:
package tech.msop.distributed.lock.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("db_lock")
public class LockEntity {
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 锁名
*/
private String lockName;
/**
* 类名
*/
private String className;
/**
* 方法名
*/
private String methodName;
/**
* 服务器IP
*/
private String serverName;
/**
* 线程名
*/
private String threadName;
/**
* 获得锁时间
*/
private Date createTime;
/**
* 描述
*/
private String desc;
}
新增Mapper文件
package tech.msop.distributed.lock.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import tech.msop.distributed.lock.entity.LockEntity;
public interface LockMapper extends BaseMapper<LockEntity> {
}
改造服务方法,支持MySQL分布式锁
package tech.msop.distributed.lock.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.locks.*;
import org.apache.curator.framework.recipes.shared.SharedCount;
import org.redisson.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import tech.msop.distributed.lock.constants.StockConstant;
import tech.msop.distributed.lock.entity.LockEntity;
import tech.msop.distributed.lock.entity.StockEntity;
import tech.msop.distributed.lock.lock.DistributedLockClient;
import tech.msop.distributed.lock.lock.DistributedRedisLock;
import tech.msop.distributed.lock.mapper.LockMapper;
import tech.msop.distributed.lock.mapper.StockMapper;
import tech.msop.distributed.lock.service.IStockService;
import tech.msop.distributed.lock.zk.ZkClient;
import java.util.Date;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
* 库存服务实现类 <br/>
*/
@Service
@Slf4j
public class StockServiceImpl extends ServiceImpl<StockMapper, StockEntity>
implements IStockService {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private DistributedLockClient distributedLockClient;
@Autowired
private RedissonClient redissonClient;
@Autowired
private ZkClient zkClient;
@Autowired
private CuratorFramework curatorFramework;
@Autowired
private LockMapper lockMapper;
/**
* 减库存
*/
@Override
public void checkAndLock() {
// 加锁
LockEntity lock = new LockEntity();
lock.setLockName("lock");
lock.setClassName(this.getClass().getName());
lock.setCreateTime(new Date());
try {
this.lockMapper.insert(lock);
// 1. 查询库存信息
String stock = redisTemplate.opsForValue().get("stock").toString();
// 2. 判断库存是否充足
if (stock != null && stock.length() != 0) {
Integer st = Integer.valueOf(stock);
if (st > 0) {
// 3.扣减库存
redisTemplate.opsForValue().set("stock", String.valueOf(--st));
}
}
// 释放锁
lockMapper.deleteById(lock.getId());
} catch (Exception ex) {
// 获取锁失败,重试
try {
Thread.sleep(50);
this.checkAndLock();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
缺陷及解决方案
缺点:
- 1、这把锁依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。
- 解决方案:给 锁 数据库搭建主备
- 2、这把锁没有失效世家你,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再或得到锁
- 解决方案:只要做一个定时任务,每隔一定时间就把数据库中的超时数据清理一遍
- 3、这把锁是非重入的,同一个线程在释放锁之前无法再次获取该锁。因为数据库中数据已经存在了
- 解决方案:记录获取锁的主机信息和线程信息,如果相同线程要获取锁,直接重入。
- 4、受限于数据库性能,并发能力有限
- 解决方案:无法解决
MySQL分布式锁总结
- 独占排他互斥使用:借助唯一键索引
- 防死锁:
- 客户端程序获取到锁之后,客户端程序的服务器宕机。给锁记录添加一个获取锁时间列,额外的定时器检查获取锁的系统时间和当前时间的差值是否超过了阈值
- 不可重入:通过记录服务器信息、线程信息与重入次数实现可重入性
- 防误删:借助于ID的唯一性防止误删除
- 原子性:一个写操作,还可以借助于MySQL的悲观锁实现
- 可重入:通过记录服务器信息、线程信息与重入次数实现可重入性
- 自动续期:服务器内的定时器重置获取锁的系统时间
- 单机故障:搭建MySQL主备
- 集群情况下锁机制失效问题。
分布式锁总结
三种方式实现分布式锁的依据:
- Redis:基于Key的唯一性
- Zookeeper:基于znode节点的唯一性
- MySQL:基于唯一键索引
实现复杂性或者难度角度:Zookeeper > Redis > 数据库
实际性能角度:Redis > Zookeeper > 数据库
可靠性角度:Zookeeper > Redis = 数据库
这三种方式都不是尽善尽美,我们可以根据实际业务情况选择最适合的方案:
如果追求极致性能可以选择:Redis
如果追求可靠性可以选择:Zookeeper
实现独占排他,对性能 对可靠性要求都不高的情况下,只是简单了解,可以选择:MySQL
常见锁分类:
- 悲观锁: 具有强烈的独占和排他特性,在整个数据处理过程中,将数据处于锁定状态。适合于写比较多,会阻塞读操作。
- 乐观锁: 采取了更加宽松的加锁机制,大多是基于数据版本(Version)及时间戳来实现。适合于读比较多,不会阻塞读。
- 独占锁、互斥锁、排他锁: 保证在任一时刻,只能被一个线程独占排他持有。如Java中的synchronized、ReentrantLock
- 共享锁: 可同时被多个线程共享持有。如CountDownLatch倒计数器、Semaphore信号量。
- 可重入锁: 又名递归锁。同一个线程在外层方法获取锁的时候,在进入内层方法时会自动获取锁。
- 不可重入锁: 例如早期的synchronized
- 公平锁: 有优先级的锁,先来先得,谁先申请锁就先获取到锁。
- 非公平锁: 无优先级的锁,后来者也机会先获取到锁。
- 自旋锁: 当线程尝试获取锁失败时(锁已经被其他线程占用了),无限循环重试获取到锁。
- 阻塞锁: 当线程尝试获取锁失败时,线程进入阻塞状态,直到接受信号被唤醒。在竞争激烈情况下,性能较高。
- 读锁: 共享锁。
- 写锁: 独占排他锁。
- 偏向锁: 一直被一个线程所访问,那么该线程会自动获取锁。
- 轻量级锁(CAS): 当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
- 重量级锁: 当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋到一定次数的时候(10次),还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让他申请的线程进入阻塞,性能降低。
以上其实就是synchronized的锁升级过程
- 表级锁: 对整张表加锁,加锁快开销小,不会出现死锁,但并发度低,会增加锁冲突的概率。
- 行级锁: 是MySQL粒度最小的锁,只针对操作行,可大大减少锁冲突概率,并发度高,但加锁慢,开销大,会出现死锁。