在讨论分布式锁原理的时候,我们带着如下思考进入今天的主题:
- 根据业务场景如何选择分布式锁模型:CP还是AP?
- 读锁、加锁是否是原子操作?
- 锁能否正常释放,避免死锁(如添加过期时间)
- 保证分布式锁高可用:如何保证锁资源不丢失(保存锁的机器可能宕机)
锁的本质就是对共享资源的串行化处理。在单进程环境中,Java JDK提供了两种互斥锁实现:Lock和Synchronized。这两种锁对共享资源的操作前后加解锁,保证不同线程可以互斥有序的操作共享资源。
在分布式环境下,由于不同主机之间无法直接访问共享资源,所以就需要我们自己来实现分布式锁,保证不同JVM、不同主机之间不会出现资源抢占。
一、分布式锁基本条件
实现分布式锁有以下基本条件:
- 存储空间:锁的实现需要有一个可以存储锁的空间。在多线程中可以使用内存保存锁;多进程中可以用共享的内存或者磁盘中的文件当做锁;分布式环境中,不同主机无法访问对方的内存或磁盘文件,所以要使用外部存储空间来存储锁。常见的有数据库、缓存如Redis、MongoDB、ZK等;
- 唯一标识:分布式环境中要保证锁的名称是全局唯一的;
- 两种状态:锁需要有两种不同的状态,如加锁、解锁;存在、不存在等;
最简单的实现可以用数据库实现一个简单的分布式锁:
建表:
CREATE TABLE `tb_lock` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`resource` bigint(64) NOT NULL COMMENT '锁定的资源',
`status` tinyint(2) NOT NULL DEFAULT '0' COMMENT '锁资源状态:(0:解锁,1:加锁)',
`desc` varchar(120) NOT NULL DEFAULT "" COMMENT '描述',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_idx_resource` (`resource`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据库分布式锁表';
伪代码:
// 1、读锁 (resourceId对应数据库resource字段,可以为用户编号、订单编号等其他场景)
lock = mysql.getByResourceId(resourceId);
// 2、判断锁状态(lock.status = 1是否为加锁状态 ,是表示锁被占用, sleep后再尝试)