唯一约束 & CAS & 乐观锁

2019-04-05  本文已影响0人  用一会再说

我们相信生活是美好的,可有些事情你无法改变!

加锁是为了控制并发,保证同一时刻只能有一个线程进行操作,目的是为了保证一致性。悲观者觉得某个东西可宝贝了,看谁都觉得是要来抢他的,每次都要加把锁才进行操作;乐观者未必不觉得是个宝贝,只是于他而言,与他人无关,所以从不加锁。悲观锁和乐观锁只是不同思想,适用不同场景,无优劣之分!

讨论加锁前,先讨论不加锁!

唯一约束

在某些场景中,我们限定某些东西唯一,比如网站注册的用户名或者手机号。mysql层面有以下几种方式保证唯一性。

create table user (
  id bigint(20) not null,
  name varchar(64)  not null unique comment '用户名', #字段定义上加‘unique’唯一约束
  phone char(11) not null comment '手机号',
  primary key(`id`),                                 #字段本身为主键。主键本就是唯一索引
  unique key index_phone (phone)                     #字段上加唯一索引
) engine=innodb default charset=utf8;

如上id为主键唯一索引,name字段上唯一约束,phone上建了唯一索引;三者异同简述如下:

1、主键索引与唯一索引 
    主键索引不能为null,唯一索引可以为null
2、唯一约束和唯一索引
    唯一约束是数据库理论定义的语义,保证字段唯一
    唯一索引是唯一约束的一种实现,除了唯一限定还附带有索引提供的快速查找功能

执行如上建表语句后,再查看该表的DDL语句,如下

mysql> select VERSION();
+------------+
| VERSION()  |
+------------+
| 5.7.21-log |
+------------+
1 row in set

mysql> show CREATE TABLE user;
+-------+------------------------------------------------------------------------------------------------------------------+
| Table | Create Table                                                                                                                                                                                                                                                           |
+-------+------------------------------------------------------------------------------------------------------------------+
| user  | CREATE TABLE `user` (
  `id` bigint(20) NOT NULL,
  `name` varchar(64) NOT NULL COMMENT '用户名',
  `phone` char(11) NOT NULL COMMENT '手机号',
  PRIMARY KEY (`id`),
  UNIQUE KEY `name` (`name`),
  UNIQUE KEY `index_phone` (`phone`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 |
+-------+-------------------------------------------------------------------------------------------------------------------+
1 row in set

可以看到字段name唯一约束的实际实现采用的也是唯一索引方式,事实上我们重点关注的字段限定唯一,同时也有频繁查询的需求,所以建立索引是比较合理的方式,起码mysql这样认为。

业务上,我们要保证唯一,除了依赖数据库层面的约束外,我们在代码上有悲观和乐观两种方式。悲观者可能是这样

if (null == userService.getByName(user.getName())) {
    userService.insert(user);
}

乐观者也许是这样

try {
    userService.insert(user);
} catch (DuplicateKeyException e) {
    // 友好提示 或 重试机制
}

悲观者觉得大概率name会重复,所以要先检查是否存在,不存在才进行插入操作,尽管悲观,如上依然没有加锁;乐观者觉得name基本不会重复,先插入再说。事实上生活总是美好更多一点!

CAS

CAS是Compare and Swap,即先比较觉得OK就替换;
JAVE 5开始引入原子操作,便是基于CAS思想,可自行搜索了解!这里我想简单说一下sql语句层面运用CAS思想达到的一点应用。以订单状态流转为例,假设订单有新建、部分支付、完全支付、取消、过期几个状态,状态流转如下


image.png

忽略这张图的丑陋,我们可以看到几个限定,我们要更新状态
1、取消状态,前提是当前状态必须是新建
2、过期状态,前提是当前状态必须是新建
3、部分支付状态,前提是当前状态必须是新建
4、全部支付状态,前提是当前状态必须是新建或部分支付

悲观者的做法也许和上面用户名唯一约束是一样的,想获取要操作的订单,判断当前状态是否满足目标状态转换的前提,不满足则不进行操作,想想都会有很多繁琐的if-else,具体代码不赘述!

运用CAS的思想,也许是这样

入参
    订单ID:orderId
    当前状态列表:curStatusList
    目标状态:targetStatus
SQL:
    update t_order set status=#{targetStatus} where order_id=#{orderId} and status in (curStatusList)
业务代码:
    只需要判断上面update语句返回值是否为1(影响或匹配的行数),便可知道操作是否成功,是先斩后奏的乐观锁思想

JAVA 5 原子操作的CAS依赖sun.misc.Unsafe类,本质上依赖操作系统
这里CAS的实现依赖数据库事务的原子性,运用这样的方式可使得语句简单、语义明确

现实中,我们相信生活是美好的,乐观总是有利的,但生活不总是如此,生活总会有枷锁,可真事实如此,也不必悲观!

上一篇 下一篇

猜你喜欢

热点阅读