之前的文章《Java分布式锁实现》中列举了分布式锁的3种实现方式,分别是基于数据库实现,基于缓存实现和基于zookeeper实现。三种实现方式各有可取之处,本篇文章就详细讲解一下Java分布式锁之基于数据库的实现方式,也是最简单最易理解的实现方式。

首先,先来阐述下“锁”的概念,锁作为一种安全防御工具,既能上锁防止别人打开,又能让持有钥匙的人打开锁,这是锁的基本功能。那再来说一下“分布式锁”,分布式锁是在分布式系统(多个独立运行系统)内的锁,相对来说,这把锁的安全级别以及作用范围更大,所以从设计上就要考虑更多东西。

现在来说,怎么基于数据库实现这把分布式锁。其实说白了就是,把锁作为数据资源存入数据库,当持有这把锁的访问者来决定是否开锁。

以下详细讲解了在多个应用服务里,怎样用数据库去实现分布式锁。

结合案例:

1.客户app取出交易(同一个客户在某一个时间点只能对某种资产做取现操作)

2.交易重试补偿(交易过程服务宕机,扫描重试补偿)

一、数据库的设计

数据库锁表的表结构如下:

 
field type comment
ID bigint 主键
OUTER_SERIAL_NO varchar 流水号
CUST_NO char 客户号
SOURCE_CODE varchar 锁操作
THREAD_NO varchar 线程号
STATUS char 锁状态
REMARK varchar 备注
CREATED_AT timestamp 创建时间
UPDATED_AT timestamp 更新时间

作为锁的必要属性有5个:系统流水号,客户号,锁操作,线程号和锁状态,下面来解释一下每种属性

流水号:锁的具体指向,比如可以是产品,可以是交易流水号(后面会说到交易同步锁、交易补偿锁的使用方式)

客户号:客户的唯一标识

锁操作:客户的某种操作,比如客户取现操作,取现补偿重试操作

线程号:当前操作线程的线程号,比如取当前线程的uuid

锁状态:P处理中,F失败,Y成功

二、代码设计

代码的目录结构如下:

主要贴一下锁操作的核心代码实现:

锁接口定义:DbLockManager.java

/**
* 锁接口 <br>
*
* @Author fugaoyang
*
*/
public interface DbLockManager { /**
* 加锁
*/
boolean lock(String outerSerialNo, String custNo, LockSource source); /**
* 解锁
*/
void unLock(String outerSerialNo, String custNo, LockSource source, LockStatus targetStatus); }

锁接口实现类:DbLockManagerImpl.java

/**
*
* 数据库锁实现<br>
*
* @author fugaoyang
*
*/
@Service
public class DbLockManagerImpl implements DbLockManager { private final Logger LOG = LoggerFactory.getLogger(this.getClass()); @Autowired
private DbSyncLockMapper lockMapper; @Transactional
public boolean lock(String outerSerialNo, String custNo, LockSource source) { boolean isLock = false;
TradeSyncLock lock = null;
try {
lock = lockMapper.find(outerSerialNo, custNo, source.getCode()); if (null == lock) {
lock = new TradeSyncLock();
createLock(lock, outerSerialNo, custNo, source); int num = lockMapper.insert(lock);
if (num == 1) {
isLock = true;
} LOG.info(ThreadLogUtils.getLogPrefix() + "加入锁,客户号[{}],锁类型[{}]", custNo, source.getCode());
return isLock;
} // 根据交易类型进行加锁
isLock = switchSynsLock(lock, source);
LOG.info(ThreadLogUtils.getLogPrefix() + "更新锁,客户号[{}],锁类型[{}]", custNo, source.getCode()); } catch (Exception e) {
LOG.error(ThreadLogUtils.getLogPrefix() + "交易加锁异常, 客户号:" + custNo, e);
}
return isLock;
} @Transactional
public void unLock(String outerSerialNo, String custNo, LockSource source, LockStatus targetStatus) { try {
TradeSyncLock lock = lockMapper.find(outerSerialNo, custNo, source.getCode()); if (null != lock) {
lockMapper.update(lock.getId(), targetStatus.getName(), LockStatus.P.getName(),
ThreadLogUtils.getCurrThreadUuid(), ThreadLogUtils.getCurrThreadUuid());
} LOG.info(ThreadLogUtils.getLogPrefix() + "释放锁,客户号[{}],锁类型[{}]", custNo, source.getCode());
} catch (Exception e) {
LOG.error(ThreadLogUtils.getLogPrefix() + "释放锁异常, 客户号:{}", custNo, e);
}
} /**
* 匹配加锁
*/
private boolean switchSynsLock(TradeSyncLock lock, LockSource source) {
boolean isLock = false; switch (source) {
case WITHDRAW:
;
isLock = tradeSynsLock(lock);
break;
case WITHDRAW_RETRY:
;
isLock = retrySynsLock(lock);
break;
default:
;
}
return isLock;
} /**
* 交易同步锁
*/
private boolean tradeSynsLock(TradeSyncLock lock) {
// 处理中的不加锁,即不执行交易操作
if (LockStatus.P.getName().equals(lock.getStatus())) {
return false;
} int num = lockMapper.update(lock.getId(), LockStatus.P.getName(), LockStatus.S.getName(),
ThreadLogUtils.getCurrThreadUuid(), null);
if (num == 1) {
return true;
}
return false;
} /**
* 补偿同步锁
*/
private boolean retrySynsLock(TradeSyncLock lock) {
// 处理中或处理完成的不加锁,即不执行补偿操作
if (LockStatus.P.getName().equals(lock.getStatus()) || LockStatus.S.getName().equals(lock.getStatus())) {
return false;
} int num = lockMapper.update(lock.getId(), LockStatus.P.getName(), LockStatus.F.getName(),
ThreadLogUtils.getCurrThreadUuid(), null);
if (num == 1) {
return true;
}
return false;
} private void createLock(TradeSyncLock lock, String outerSerialNo, String custNo, LockSource source) {
lock.setOuterSerialNo(outerSerialNo);
lock.setCustNo(custNo);
lock.setSourceCode(source.getCode());
lock.setThreadNo(ThreadLogUtils.getCurrThreadUuid());
lock.setStatus(LockStatus.P.getName());
lock.setRemark(source.getDesc());
} }

获取当前线程号以及打印uuid工具类ThreadLogUtils.Java

/**
*
* 线程处理<br>
*
* @author fugaoyang
*
*/
public class ThreadLogUtils { private static ThreadLogUtils instance = null; private ThreadLogUtils() {
setInstance(this);
} // 初始化标志
private static final Object __noop = new Object();
private static ThreadLocal<Object> __flag = new InheritableThreadLocal<Object>() {
@Override
protected Object initialValue() {
return null;
}
}; // 当前线程的UUID信息,主要用于打印日志;
private static ThreadLocal<String> currLogUuid = new InheritableThreadLocal<String>() {
@Override
protected String initialValue() {
return UUID.randomUUID().toString()/* .toUpperCase() */;
}
}; private static ThreadLocal<String> currThreadUuid = new ThreadLocal<String>() {
@Override
protected String initialValue() {
return UUIDGenerator.getUuid();
}
}; public static void clear(Boolean isNew) {
if (isNew) { currLogUuid.remove(); __flag.remove(); currThreadUuid.remove(); }
} public static String getCurrLogUuid() {
if (!isInitialized()) {
throw new IllegalStateException("TLS未初始化");
} return currLogUuid.get();
} public static String getCurrThreadUuid() {
return currThreadUuid.get();
} public static void clearCurrThreadUuid() {
currThreadUuid.remove();
} public static String getLogPrefix() {
if (!isInitialized()) {
return "";
} return "<uuid=" + getCurrLogUuid() + ">";
} private static boolean isInitialized() {
return __flag.get() != null;
} /**
* 初始化上下文,如果已经初始化则返回false,否则返回true<br/>
*
* @return
*/
public static boolean initialize() {
if (isInitialized()) {
return false;
} __flag.set(__noop);
return true;
} private static void setInstance(ThreadLogUtils instance) {
ThreadLogUtils.instance = instance;
} public static ThreadLogUtils getInstance() {
return instance;
} }

两种锁的实现的大致思路如下:

1.交易同步锁

当一个客户在app取现,第一次进入时,会插入一条当前线程,状态是P,操作是取现的锁,取现成功后根据当前线程号会更新成功;

当一个客户同时多个取现操作时,只有一个取现操作会加锁成功,其它会加锁失败;

当一个客户已经在取现中,这时数据库已经有一条状态P的锁,该客户同时又做了取现,这个取现动作会尝试加锁而退出;

2.交易重试补偿锁

1.当一个客户取现加锁成功,因调用第三方支付接口超时时,后台会对该笔交易重新发起重试打款操作,这时会新加一条当前交易流水号,当前线程号,状态是P,操作是取现重试的锁,重试的支付结果是成功的话,更新该条锁数据为Y状态,否则更新该条数据为F状态;

2.当重试支付失败后,再去重试打款时,发现锁的状态是F,这时把F更新为P,继续重试,根据重试结果更新锁状态。

上面实现的是一个最基本的数据库分布式锁,满足的并发量也是基于数据库所能扛得住的,性能基本可以满足普通的交易量。

后续可以优化的部分:

1.当一个用户同时多次获取lock时,因为目前是用的乐观锁,只会有一个加锁成功,可以优化成加入while(true)循环获取lock,当失败次数到达指定次数时退出,当前的操作结束。

2.当锁表数据量随着时间增大时,可以考虑按用户对锁表进行分表分库,以减小数据库方面的压力。

3.对锁的操作可以抽象出来,作为抽象实现,比如具体的取现操作只关心取现这个业务实现。

因为时间有限,写的比较仓促,希望大家有问题可以提出,相互探讨~~

完整示例代码后续会更新到github。



Java分布式锁之数据库实现的更多相关文章

  1. Java分布式锁之数据库方式实现

    之前的文章<Java分布式锁实现>中列举了分布式锁的3种实现方式,分别是基于数据库实现,基于缓存实现和基于zookeeper实现.三种实现方式各有可取之处,本篇文章就详细讲解一下Java分 ...

  2. Java分布式:分布式锁之数据库实现

    Java分布式:分布式锁之数据库实现 分布式锁系列教程重点分享锁实现原理 锁实现原理 创建一张名为methodLock的数据库表,为方法名字段(method_name)添加唯一性约束. CREATE ...

  3. java 分布式锁

    转自:http://www.hollischuang.com/archives/1716 目前几乎很多大型网站及应用都是分布式部署的,分布式场景中的数据一致性问题一直是一个比较重要的话题.分布式的CA ...

  4. Java分布式锁实现详解

    在进行大型网站技术架构设计以及业务实现的过程中,多少都会遇到需要使用分布式锁的情况.那么问题也就接踵而至,哪种分布式锁更适合我们的项目? 下面就这个问题,我做了一些分析: 分布式锁现状: 目前几乎很多 ...

  5. Java分布式锁,搞懂分布式锁实现看这篇文章就对了

    随着微处理机技术的发展,人们只需花几百美元就能买到一个CPU芯片,这个芯片每秒钟执行的指令比80年代最大的大型机的处理机每秒钟所执行的指令还多.如果你愿意付出两倍的价钱,将得到同样的CPU,但它却以更 ...

  6. Java分布式锁看这篇就够了

    ### 什么是锁? 在单进程的系统中,当存在多个线程可以同时改变某个变量(可变共享变量)时,就需要对变量或代码块做同步,使其在修改这种变量时能够线性执行消除并发修改变量. 而同步的本质是通过锁来实现的 ...

  7. Java分布式锁

    分布式锁简述 在单机时代,虽然不存在分布式锁,但也会面临资源互斥的情况,只不过在单机的情况下,如果有多个线程要同时访问某个共享资源的时候,我们可以采用线程间加锁的机制,即当某个线程获取到这个资源后,就 ...

  8. Java分布式锁的三种实现方案(redis)

    方案一:数据库乐观锁 乐观锁通常实现基于数据版本(version)的记录机制实现的,比如有一张红包表(t_bonus),有一个字段(left_count)记录礼物的剩余个数,用户每领取一个奖品,对应的 ...

  9. Java分布式锁三种实现方案

    方案一:数据库乐观锁 乐观锁通常实现基于数据版本(version)的记录机制实现的,比如有一张红包表(t_bonus),有一个字段(left_count)记录礼物的剩余个数,用户每领取一个奖品,对应的 ...

随机推荐

  1. java 操作hbase1.2

    说明: .第一部分为代码 .第二部分为工程pom文件 [java] view plain copy import org.apache.hadoop.conf.Configuration; impor ...

  2. hdu 3001 Travelling(状态压缩 三进制)

    Travelling Time Limit: 6000/3000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)Total ...

  3. rsync服务精讲 -- 视频

    rsync服务 开源数据同步工具rsync视频(老男孩分享) 浏览网址 01-rsync基础介绍 http://oldboy.blog.51cto.com/2561410/1216550 11-rsy ...

  4. onload、DOMContentLoaded与性能问题

    onload.DOMContentLoaded与性能问题 onload事件 DomContentLoaded   1.onload事件 onload事件一般在所有的文档内容加载完成后触发,如果网页中图 ...

  5. JAVAscript学习笔记 jsDOM 第五节 (原创) 参考js使用表

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  6. IE (6-11)版本,在使用iframe的框架时,通过a标签javascript:; 和js跳转parent.location的时候 出现在新页面打开的情况

    问题描述: 使用iframe的情况下,在子框架中,使用如下形式的跳转: <a href="javascript:;" onclick="parent.locatio ...

  7. javaweb部署多个项目(复制的项目)

    最近需要在一台服务器部署两个已经编译完了的javaweb项目,但是因为项目名一样,仅修改文件夹的名字无法实现两个项目共存,最后只能考虑采用部署多个tomcat服务器的方法来实现.搜索后终于找到个好方法 ...

  8. Java设计模式相关面试

    1.接口是什么?为什么要使用接口而不是直接使用具体类? 接口用于定义 API.它定义了类必须得遵循的规则.同时,它提供了一种抽象,因为客户端只使用接口,这样可以有多重实现,如 List 接口,你可以使 ...

  9. Vue.js优雅的实现列表清单

        一.Vue.js简要说明 Vue.js (读音 /vjuː/) 是一套构建用户界面的渐进式框架.与前端框架Angular一样, Vue.js在设计上采用MVVM模式,当View视图层发生变化时 ...

  10. 主机和VMware中的Linux如实现共享文件夹

    当我在网上查了几小时的挂载文件夹方法后发现,VMware中的Linux的挂载和双系统的挂载不同 最终目的就是在/mnt目录下有个hgfs的文件夹 效果图: 首先打开VMware中的Linux系统 具体 ...