java分布式锁 - 基于数据库表实现乐观锁(三)

前言

前几天写过使用zookeeper、redis和数据库悲观锁实现分布式锁,好吧,今天想再研究一下使用数据库表示实现。

原理

先在数据库建一张资源表,包含资源名称resource_name、版本号version(可以是随机数、时间戳,与锁是否过期有关)等信息,将资源名称建立唯一索引

在程序尝试上锁时,根据资源名称查询数据,获取锁的版本号也就是时间戳

将当前时间与版本号作比较,如果当前时间大于等于版本号的时间,就以资源名称和查出来的版本号作为条件,更新数据库版本号为自己的版本号

自己的版本号怎么产生的呢?就是当前时间戳加上超时时间,这里我设置的超时时间为2000毫秒

如果更新返回的影响行数大于0就是更新成功,该线程获取了锁;如果更新返回的影响行数不大于0就是更新失败,该线程无法获取锁,然后睡眠一定时间再重试

我们可以设置重试时间,如果超过时间就不再获取锁了

如果版本号已经被更新了的话,那么其他线程以查出来的旧版本号就无法匹配得到数据行了,在这个过程中只有一个线程能够更新

释放锁的过程就是将版本号改为当前时间戳

在这里插入图片描述

建立数据表

CREATE TABLE `resource` (
  `id` int(4) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `resource_name` varchar(64) NOT NULL DEFAULT '' COMMENT '资源名',
  `share` varchar(64) NOT NULL DEFAULT '' COMMENT '状态',
    `version` int(4) NOT NULL DEFAULT '' COMMENT '版本号',
  `desc` varchar(1024) NOT NULL DEFAULT '备注信息',
  `update_time` timestamp NOT NULL DEFAULT '' COMMENT '保存数据时间,自动生成',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uidx_resource_name` (`resource_name `) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='资源';

创建数据库工具类


package com.tp.database;

import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.pool.DruidDataSourceFactory;
import org.slf4j.LoggerFactory;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

public class Database {

    private static DataSource dataSource = null;

    static {
        Map properties = new HashMap<String, String>();
        properties.put(DruidDataSourceFactory.PROP_DRIVERCLASSNAME, "com.mysql.jdbc.Driver");
        properties.put(DruidDataSourceFactory.PROP_URL, "jdbc:mysql://localhost:3306/extmail?useUnicode=true&characterEncoding=utf8");
        properties.put(DruidDataSourceFactory.PROP_USERNAME, "root");
        properties.put(DruidDataSourceFactory.PROP_PASSWORD, "root");
        properties.put(DruidDataSourceFactory.PROP_MAXACTIVE, "100");
        properties.put(DruidDataSourceFactory.PROP_INITIALSIZE, "1");
        properties.put(DruidDataSourceFactory.PROP_MAXWAIT, "60000");
        properties.put(DruidDataSourceFactory.PROP_TIMEBETWEENEVICTIONRUNSMILLIS, "60000");
        properties.put(DruidDataSourceFactory.PROP_MINEVICTABLEIDLETIMEMILLIS, "300000");
        properties.put(DruidDataSourceFactory.PROP_VALIDATIONQUERY, "select 1 from dual");
        properties.put(DruidDataSourceFactory.PROP_TESTWHILEIDLE, "true");
        properties.put(DruidDataSourceFactory.PROP_TESTONBORROW, "false");
        properties.put(DruidDataSourceFactory.PROP_TESTONRETURN, "false");
        properties.put(DruidDataSourceFactory.PROP_MAXOPENPREPAREDSTATEMENTS, "20");
        try {
            dataSource =  DruidDataSourceFactory.createDataSource(properties);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 插入或更新数据库
     * @param sql
     * @return
     */
    public static Integer insertOrUpdate(String sql){
        Integer flag = 0;
        Connection connection = null;
        PreparedStatement ps = null;

        try{
            connection = dataSource.getConnection();
            ps = connection.prepareStatement(sql);
            flag = ps.executeUpdate();
        }catch (Exception e){
            //e.printStackTrace();
        }finally {
            if (connection!=null){
                try{
                    connection.close();
                }catch (Exception e){
                    connection = null;
                }
            }
            if (ps!=null){
                try{
                    ps.close();
                }catch (Exception e){
                    ps = null;
                }
            }
        }

        return flag;
    }

    /**
     * 插入或更新数据库
     * @param resouceName
     * @return
     */
    public static Map<String,Object> selectByResourceName(String resouceName){
        Map result = null;
        Connection connection = null;
        PreparedStatement ps = null;
        ResultSet rs = null;

        String sql = "select id,resource_name,share,version,`desc`,update_time from resource where resource_name = ?";

        try{
            connection = dataSource.getConnection();
            ps = connection.prepareStatement(sql);
            ps.setString(1,resouceName);
            rs = ps.executeQuery();
            if (rs.next()){
                result = new HashMap();
                result.put("id",rs.getInt(1));
                result.put("resource_name",rs.getString(2));
                result.put("share",rs.getString(3));
                result.put("version",rs.getLong(4));
                result.put("desc",rs.getString(5));
                result.put("update_time",rs.getTimestamp(6));
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            if (connection!=null){
                try{
                    connection.close();
                }catch (Exception e){
                    connection = null;
                }
            }
            if (ps!=null){
                try{
                    ps.close();
                }catch (Exception e){
                    ps = null;
                }
            }
            if (rs!=null){
                try{
                    rs.close();
                }catch (Exception e){
                    rs = null;
                }
            }
        }

        return result;
    }
}

创建锁工具类

在这里我是没有插入资源数据,一张空表,如果查询资源为空的话就会插入一条资源,因为刚开始有很多线程插表,由于资源名称建立了唯一索引,就会报错,但是不影响使用,符合逻辑


package com.tp.database;

import com.sun.org.apache.xpath.internal.operations.Bool;
import org.omg.CORBA.TIMEOUT;

import javax.sql.DataSource;
import javax.swing.text.StyledEditorKit;
import java.util.Map;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

public class MyLock {

    private static final String resourceName = "myresource";

    private static final Integer TIMEOUT = 10 * 1000;//超时10s

    private static final Integer STEPTIME = 100;//每100毫秒尝试获取一次锁

    private static final Integer RELEASETIME = 2*1000;//释放锁的时间

    private Long version = null;

    private String owner = null;

    /**
     * 获取锁
     */
    public void tryLock(){
        Integer timeOut = TIMEOUT;
        while (timeOut > 0){
            Long now = System.currentTimeMillis();
            version = now + RELEASETIME;
            Map<String,Object> result = Database.selectByResourceName(resourceName);
            if (result==null){
                //没有被获取锁,直接尝试获取锁
                String sql = "insert into resource(resource_name,share,version,`desc`,update_time) values('"+resourceName+"','1',"+version+",'获取了锁',now())";
                Integer flag = Database.insertOrUpdate(sql);
                if (flag > 0){
                    System.out.println("获取锁成功...,version = " + version);
                    return;
                }
            } else {
                //如果有线程获取锁,判断该锁是否超时
                Long oldVersion = (Long) result.get("version");
                if (now>=oldVersion){
                    //锁超时了,直接获得锁
                    String sql = "update resource set version = " + version + " where resource_name ='" + resourceName + "' and version = " + oldVersion;
                    Integer flag = Database.insertOrUpdate(sql);
                    if (flag>0){
                        System.out.println("获取锁(超时)成功,version = " + version);
                        return;
                    }
                }
            }

            try{
                Thread.sleep(STEPTIME);
            }catch (Exception e){
                e.printStackTrace();
            }

            timeOut = timeOut - STEPTIME;
        }
    }

    /**
     * 释放锁
     */
    public void unLock(){
        String sql = "update resource  set version = "+System.currentTimeMillis()+" where resource_name = '" + resourceName +"' and version = " + version;
        Integer flag = Database.insertOrUpdate(sql);
        if (flag > 0){
            System.out.println("释放锁成功,version = " + version);
        }else{
            System.out.println("释放锁失败,version = " + version);
        }
    }
}

测试类


package com.tp.database;

public class LockTest {
    public static void main(String[] args) {
        for (int i=0;i<100;i++){
            new Thread(){
                @Override
                public void run() {
                    MyLock myLock = new MyLock();
                    myLock.tryLock();
                    System.out.println("=======乐观锁处理业务=======");
                    myLock.unLock();
                }
            }.start();
        }
    }
}

结果

获取锁(超时)成功,version = 1587728751610
=======乐观锁处理业务=======
释放锁成功,version = 1587728751610
获取锁(超时)成功,version = 1587728755892
=======乐观锁处理业务=======
释放锁成功,version = 1587728755892
获取锁(超时)成功,version = 1587728758461
=======乐观锁处理业务=======
释放锁成功,version = 1587728758461
获取锁(超时)成功,version = 1587728760054
=======乐观锁处理业务=======
释放锁成功,version = 1587728760054
获取锁(超时)成功,version = 1587728761553
=======乐观锁处理业务=======
释放锁成功,version = 1587728761553
获取锁(超时)成功,version = 1587728762604
=======乐观锁处理业务=======
释放锁成功,version = 1587728762604

总结

使用数据表实现与之前用redis实现差不多,都是锁定资源和版本号,通过版本号来判断是否超时。

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值