不管是jvm锁还是mysql锁,为了保证线程的并发安全,都提供了悲观独占排他锁。所以独占排他也是分布式锁的基本要求。本文介绍基于mysql实现分布式锁,不建议应用于生产项目中。

① 数据结构

可以利用唯一键索引不能重复插入的特点实现。设计表如下:

CREATE TABLE `tb_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;
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

Lock实体类:

@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("tb_lock")
public class Lock {

    private Long id;
    private String lockName;
    private String className;
    private String methodName;
    private String serverName;
    private String threadName;
    private Date createTime;
    private String desc;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.

LockMapper接口:

public interface LockMapper extends BaseMapper<Lock> {
}
  • 1.
  • 2.

② 设计与实现

synchronized关键字和ReetrantLock锁都是独占排他锁,即多个线程争抢一个资源时,同一时刻只有一个线程可以抢占该资源,其他线程只能阻塞等待,直到占有资源的线程释放该资源。

  1. 线程同时获取锁(insert)
  2. 获取成功,执行业务逻辑,执行完成释放锁(delete)
  3. 其他线程等待重试

代码实现:

@Service
public class StockService {

    @Autowired
    private StockMapper stockMapper;

    @Autowired
    private LockMapper lockMapper;

    /**
     * 数据库分布式锁
     */
    public void checkAndLock() {

        // 加锁
        Lock lock = new Lock(null, "lock", this.getClass().getName(), new Date(), null);
        try {
            this.lockMapper.insert(lock);
        } catch (Exception ex) {
            // 获取锁失败,则重试
            try {
                Thread.sleep(50);
                this.checkAndLock();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        // 先查询库存是否充足
        Stock stock = this.stockMapper.selectById(1L);

        // 再减库存
        if (stock != null && stock.getCount() > 0){

            stock.setCount(stock.getCount() - 1);
            this.stockMapper.updateById(stock);
        }

        // 释放锁
        this.lockMapper.deleteById(lock.getId());
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.

加锁:

// 加锁
Lock lock = new Lock(null, "lock", this.getClass().getName(), new Date(), null);
try {
    this.lockMapper.insert(lock);
} catch (Exception ex) {
    // 获取锁失败,则重试
    try {
        Thread.sleep(50);
        this.checkAndLock();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.

解锁:

// 释放锁
this.lockMapper.deleteById(lock.getId());
  • 1.
  • 2.

③ 缺陷及解决方案

  1. 这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。
    解决方案:给 锁数据库 搭建主备
  2. 这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。
    解决方案:只要做一个定时任务,每隔一定时间把数据库中的超时数据清理一遍。
  3. 这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。
    解决方案:记录获取锁的主机信息和线程信息,如果相同线程要获取锁,直接重入。
  4. 受制于数据库性能,并发能力有限。
    解决方案:无法解决。

④ ZK/Redis/MySQL锁对比

实现的复杂性或者难度角度:Zookeeper > redis > 数据库

实际性能角度:redis > Zookeeper > 数据库

可靠性角度:Zookeeper > redis = 数据库

这三种方式都不是尽善尽美,我们可以根据实际业务情况选择最适合的方案:

如果追求极致性能可以选择:reds方案

如果追求可靠性可以选择:zk

⑤ 锁总结

悲观锁:具有强烈的独占和排他特性,在整个数据处理过程中,将数据处于锁定状态。适合于写比较多,会阻塞读操作。
乐观锁:采取了更加宽松的加锁机制,大多是基于数据版本( Version )及时间戳来实现。。适合于读比较多,不会阻塞读

独占锁、互斥锁、排他锁:保证在任一时刻,只能被一个线程独占排他持有。synchronized、ReentrantLock
共享锁:可同时被多个线程共享持有。CountDownLatch到计数器、Semaphore信号量

可重入锁:又名递归锁。同一个线程在外层方法获取锁的时候,在进入内层方法时会自动获取锁。
不可重入锁:例如早期的synchronized

公平锁:有优先级的锁,先来先得,谁先申请锁就先获取到锁
非公平锁:无优先级的锁,后来者也有机会先获取到锁

自旋锁:当线程尝试获取锁失败时(锁已经被其它线程占用了),无限循环重试尝试获取锁
阻塞锁:当线程尝试获取锁失败时,线程进入阻塞状态,直到接收信号后被唤醒。在竞争激烈情况下,性能较高

读锁:共享锁
写锁:独占排他锁

偏向锁:一直被一个线程所访问,那么该线程会自动获取锁
轻量级锁(CAS):当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
重量级锁:当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候(10次),还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让他申请的线程进入阻塞,性能降低。
以上其实是synchronized的锁升级过程


表级锁:对整张表加锁,加锁快开销小,不会出现死锁,但并发度低,会增加锁冲突的概率
行级锁:是mysql粒度最小的锁,只针对操作行,可大大减少锁冲突概率,并发度高,但加锁慢,开销大,会出现死锁