java|分布式锁-使用DB 实现

DB分布式锁 多线程情况下对共享资源的操作需要加锁,避免数据被写乱,在分布式系统中,这个问题也是存在的,此时就需要一个分布式锁服务。常见的分布式锁实现一般是基于DB、Redis、zookeeper。下面笔者会按照顺序分析下这3种分布式锁的设计与实现。
分布式锁的实现由多种方式,但是不管怎样,分布式锁一般要有以下特点:

  • 排他性:任意时刻,只能有一个client能获取到锁
  • 容错性:分布式锁服务一般要满足AP,也就是说,只要分布式锁服务集群节点大部分存活,client就可以进行加锁解锁操作
  • 避免死锁:分布式锁一定能得到释放,即使client在释放之前崩溃或者网络不可达
除了以上特点之外,分布式锁最好也能满足可重入、高性能、阻塞锁特性(AQS这种,能够及时从阻塞状态唤醒)。
DB 实现方式: 下面就使用数据库的方式来实现一下分布式锁,使用下属方案存在一些问题,代码还是需要改进,请谨慎用于生成。
mavn 依赖
mysql mysql-connector-java 5.1.6

DBLock 锁实现
import java.sql.Connection; import java.sql.SQLException; import java.sql.Statement; public class DbLock {// 插入数据库的值 private static final int LOCK_ID = 1; // 非阻塞式加锁public boolean tryLock() {boolean bret = false; try {Statement st = null; Connection conn = JdbcUtils.getConnection(); // 编写sql String sql = "INSERT INTO db_lock (id) VALUES (1)"; st = conn.createStatement(); st.execute(sql); JdbcUtils.close(conn, null); bret = true; } catch (Exception ex) { return bret; } return bret; }/** * 数据据库锁 */ public void lock() {// 尝试加锁 if (tryLock()) { return; } // 如果没有成功等待,进行重试 waitLock(); // 递归调用再次尝试枷锁 lock(); }// 让当前线程休眠进行重试public void waitLock() {try {Thread.currentThread().sleep(10); } catch (Exception ex) { System.out.println(ex.getMessage()); } }/** *释放锁 */ public void unlock(){try{ Statement st = null; Connection conn = JdbcUtils.getConnection(); st = conn.createStatement(); // 编写sql String sql = "delete from db_lock where id = "+LOCK_ID; st.execute(sql); JdbcUtils.close(conn, null); }catch (SQLException e){ e.printStackTrace(); } }}

db 工具类 (数据库脚本就不提供了)
package com.qiku.study.db; import java.sql.*; public class JdbcUtils {// 可以把几个字符串定义成常量:用户名,密码,URL,驱动类 private static final String USER = "root"; private static final String PWD = "root"; private static final String URL = "jdbc:mysql://10.0.0.1:3306/db_lock"; private static final String DRIVER = "com.mysql.jdbc.Driver"; /** * 注册驱动(可以省略) */ static { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { e.printStackTrace(); } }/** * 得到数据库的连接 */ public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PWD); }/** * 关闭所有打开的资源 */ public static void close(Connection conn, Statement stmt){ if(stmt != null) { try { stmt.close(); } catch (SQLException e) { e.printStackTrace(); } } if(conn != null) { try { conn.close(); }catch (SQLException e) { e.printStackTrace(); } }}/** * 关闭所有打开的资源 重载 */ public static void close(Connection conn, Statement stmt, ResultSet rs) { if(rs != null) { try { rs.close(); } catch (SQLException e) { e.printStackTrace(); } }close(conn, stmt); } }

测试代码如下:
import com.qiku.study.db.DbLock; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; public class DBTickerTest {private int count = 100; DbLock dbLock = new DbLock(); @Test publicvoid tickeTest() throws Exception {TickRunnable tr = new TickRunnable(); new Thread(tr,"窗口A").start(); new Thread(tr,"窗口B").start(); new Thread(tr,"窗口c").start(); new Thread(tr,"窗口d").start(); new Thread(tr,"窗口e").start(); new Thread(tr,"窗口f").start(); new Thread(tr,"窗口g").start(); Thread.sleep(50000); }public class TickRunnable implements Runnable {@Override public void run() {while (count>0) { dbLock.lock(); try { if(count>0){ System.out.println(Thread.currentThread().getName()+"售出:"+ count -- + " 张票"); } }finally { dbLock.unlock(); }} } } }

DB 实现分布式锁方案缺点如下:
缺点:
1、这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。
2、这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。
3、这把锁只能是非阻塞的,因为数据的insert操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作。
4、这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。
【java|分布式锁-使用DB 实现】解决方案:
1、数据库是单点?搞两个数据库,数据之前双向同步。一旦挂掉快速切换到备库上。
2、没有失效时间?只要做一个定时任务,每隔一定时间把数据库中的超时数据清理一遍。
3、非阻塞的?搞一个while循环,直到insert成功再返回成功。
4、非重入的?在数据库表中加个字段,记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了。

    推荐阅读