java分布式锁 - 基于数据库悲观锁实现(二)

前言

前面写过了分布式锁zookeeper和redis实现,现在也一并研究了一下数据库实现方式

原理

在查询语句后面加上FOR UPDATE,数据库会在查询过程中给数据库表增加悲观锁,也称排他锁。

当某条记录被加上悲观锁之后,其他线程也就无法再改行上的数据。

在使用悲观锁的同时,我们需要注意一下锁的级别。Mysql innodb引起在加锁的时候,只有明确的指定主键(或索引)的才会执行行锁(只锁住被选取的数据),否则mysql将会执行表锁(将整个表锁住)。

在使用悲观锁时,我们必须关闭mysql数据库的自动提交属性,因为mysql默认使用自动提交模式。

你也可以在数据库客户端工具上测试出来这个效果,当在一个终端执行了 for update,不提交事务。在另外的终端上执行相同条件的 for update,会一直卡着,转圈圈…

使用数据库悲观锁做分布式锁的基本逻辑就是:在使用for update获得锁后执行相应的业务逻辑,执行完之后再使用commit来释放锁。

建立数据表


CREATE TABLE `resource_lock` (
  `id` int(4) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `resource_name` varchar(64) NOT NULL DEFAULT '' COMMENT '锁定的资源名',
  `owner` varchar(64) NOT NULL DEFAULT '' COMMENT '锁拥有者',
  `desc` varchar(1024) NOT NULL DEFAULT '备注信息',
  `update_time` timestamp NOT NULL DEFAULT now() COMMENT '保存数据时间,自动生成',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uidx_resource_name` (`resource_name`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='锁定中的资源';

数据准备

数据库中插入一条语句。因为数据库悲观锁,必须在查询到数据的时候才会加锁,如果没有查询到数据就不会加锁了,所以,必须先在数据库插入数据。

INSERT into resource_lock(resource_name,OWNER,update_time) VALUES('lock','me',now())

创建工具类


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, "123456");
        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 owner
     * @return
     */
    public static void lock(String owner){
        Map result = null;
        Connection connection = null;
        PreparedStatement ps = null;
        ResultSet rs = null;

        String sql = "select id,resource_name,owner,`desc`,update_time from resource_lock where resource_name = 'lock' for update";

        try{
            connection = dataSource.getConnection();
            connection.setAutoCommit(false);
            ps = connection.prepareStatement(sql);
            rs = ps.executeQuery();
            System.out.println("---获取锁---,owner = " + owner);

            Thread.sleep(1000);
            System.out.println("---处理业务---,owner = " + owner);

            connection.commit();
            System.out.println("---释放锁---,owner = " + owner);


        }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;
                }
            }
        }
    }
}

在这里,我们使用线程随眠来模拟实际业务。在这里,我直接根据 resource_name 字段查询数据库,该字段不是主键,应该会加表锁,而不是行锁。

测试类

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.lock();
                }
            }.start();
        }
    }
}

结果

---获取锁---,owner = d1abe4cd3cb9448ab9978b921ad47235
---处理业务---,owner = d1abe4cd3cb9448ab9978b921ad47235
---释放锁---,owner = d1abe4cd3cb9448ab9978b921ad47235
---获取锁---,owner = cb5a1350ab3d45ae9c4458820674b051
---处理业务---,owner = cb5a1350ab3d45ae9c4458820674b051
---释放锁---,owner = cb5a1350ab3d45ae9c4458820674b051
---获取锁---,owner = 0e72f9ce7b0444b59026413e2318126b
---处理业务---,owner = 0e72f9ce7b0444b59026413e2318126b
---释放锁---,owner = 0e72f9ce7b0444b59026413e2318126b
---获取锁---,owner = 00e23fd5ce33400e93391dbfbaff7745
---处理业务---,owner = 00e23fd5ce33400e93391dbfbaff7745

总结

在使用悲观锁的过程中,感觉效率还是比zookeeper和redis低下,执行起来比较慢。当创建的线程数达到100时,会有个别线程报错java.sql.SQLException: Lock wait timeout exceeded; try restarting transaction,因为在获取锁的等待时间太长了。悲观锁在获取不到锁的时候会一直阻塞,没有重试机制。悲观锁在高并发的情况下很容易遇到性能瓶颈。因此,在实际使用中不推荐使用。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值