锁机制:
乐观锁:1)通过版本号来实现,先查询获取版本号,在更新的时候校验版本号并修改。
悲观锁:同步关键字就是悲观锁,也称为排它锁。
乐观锁还让用户查询当前版本号,悲观锁如果不释放,查都不让查询。
乐观锁存在多种实现方式:mysql数据库版本号,redis实现,CAS实现等。
在并发情况下,使用锁机制,防止争抢资源。
悲观锁是对数据的修改持悲观态度(认为数据在被修改的时候一定会存在并发问题),因此在整个数据处理过程中将数据锁定。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在应用层中实现了加锁机制,也无法保证外部系统不会修改数据)
锁机制是为了解决高并发问题。
使用悲观锁的原理就是,当我们在查询出goods信息后就把当前的数据锁定,直到我们修改完毕后再解锁。
要使用悲观锁,我们必须关闭mysql数据库的自动提交属性。
set autocommit=0;
关闭了mysql的autocommit,所以需要手动控制事务的提交。
使用select…for update会把数据给锁住,不过我们需要注意一些锁的级别,MySQL InnoDB默认Row-Level Lock,所以只有「明确」地指定主键,MySQL 才会执行Row lock (只锁住被选取的数据) ,否则MySQL 将会执行Table Lock (将整个数据表单给锁住)。
如果无主键或者主键不明确,会锁住整个表。
-- 商品表
CREATE TABLE t_goods (
id int not null AUTO_INCREMENT comment 'id信息',
name VARCHAR(20) comment '商品名称',
status int comment '商品状态',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='商品表';
-- 订单表
CREATE TABLE t_orders (
id int not null AUTO_INCREMENT comment 'id信息',
goods_id int comment '商品id',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='订单表';
insert into t_goods(name,status) values('无极书',1)
insert into t_orders(name,status) values('太极',1)
-- 在高并发环境下,这三行代码可能存在问题
select status from t_goods where id=1;
insert into t_orders(goods_id) values(1);
update t_goods set status=2 where id=1;
-- 关闭事务
set autocommit=0;
start TRANSACTION;
select status from t_goods where id=1 for update;
insert into t_orders(goods_id) values(1);
update t_goods set status=2 where id=1;
commit;
set autocommit =0;
select status from t_goods where id=1 for update;
commit;
set autocommit=0;
SELECT * from t_goods where id=4 for update;
set autocommit=0;
SELECT * from t_goods where status=1 for update;
commit;
set autocommit=0;
SELECT * from t_goods where id>1 for update;
没有提交事务导致行锁住 还是因为select…for update 锁住数据?
不是因为事务,而是因为select…for update 锁住数据。
乐观锁,使用版本标识来确定读到的数据与提交时的数据是否一致。提交后修改版本标识,不一致时可以采取丢弃和再次尝试的策略。
1、悲观锁,前提是,一定会有并发抢占资源,强行独占资源,在整个数据处理过程中,将数据处于锁定状态。独占锁其实就是一种悲观锁,排它锁。
2、乐观锁,前提是,不会发生并发抢占资源,只有在提交操作的时候检查是否违反数据完整性。只能防止脏读后数据的提交,不能解决脏读。
Java里面进行多线程通信的主要方式就是共享内存的方式,共享内存主要的关注点有两个:可见性和有序性。加上复合操作的原子性,我们可以认为Java的线程安全性问题主要关注点有3个:可见性、有序性和原子性。
Java内存模型(JMM)解决了可见性和有序性的问题,而锁解决了原子性的问题。
基于redis的乐观锁实践
Redis的事务机制以及watch指令(CAS)实现乐观锁。
所谓乐观锁,就是利用版本号比较机制,只是在读数据的时候,将读到的数据的版本号一起读出来,当对数据的操作结束后,准备写数据的时候,再进行一次数据版本号的比较,若版本号没有变化,即认为数据是一致的,没有更改,可以直接写入,若版本号有变化,则认为数据被更新,不能写入,防止脏写。
基于redis实现乐观锁。
redis的事务,涉及到的指令,主要有multi,exec,discard。而实现乐观锁的指令,在事务基础上,主要是watch指令.
multi和exec之间的指令。 键值对变化时,指令不执行。
利用watch指令,基于CAS机制,简单的乐观锁。
watch指令在一次事务执行完毕后,即结束其生命周期。
基于redis的乐观锁,可以得出一个结论:
1. 乐观锁的实现,必须基于WATCH,然后利用redis的事务。
2. WATCH生命周期,只是和事务关联的,一个事务执行完毕(执行了exec命令),相应的watch的生命周期即结束。
测试redis乐观锁机制,需要开启两个窗口。
incr 命令:对数值+1;
decr 命令:对数值-1;
Redis Decrby 命令将 key 所储存的值减去指定的减量值。 decrby key 20
Redis实现乐观锁比较简单,主要思路就是watch一个key的变动,并在watch和unwatch之间做一个类似事务操作,只有当事务操作成功,整体循环才会跳出,当然,当操作期间watch的key变动时候,提交事务操作时候,事务操作将会被取消。
public void testRedisSyn(int clientName,String clientList) {
//redis中存储商品数量为(goodsNum:100)
String key = "goodsNum";
Jedis jedis = new Jedis("192.168.140.98", 6379);
jedis.auth("redis密码");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
while (true) {
try {
jedis.watch(key);
System.out.println("顾客:" + clientName + "开始抢商品");
System.out.println("当前商品的个数:" + jedis.get(key));
//当前商品个数
int prdNum = Integer.parseInt(jedis.get(key));
if (prdNum > 0) {
//开启事务,返回一个事务控制对象
Transaction transaction = jedis.multi();
//预先在事务对象中装入要执行的操作
transaction.set(key, String.valueOf(prdNum - 1));
List<Object> exec = transaction.exec();
if (exec == null || exec.isEmpty()) {
//可能是watch-key被外部修改,或者是数据操作被驳回
System.out.println("悲剧了,顾客:" + clientName + "没有抢到商品");
} else {
//这个命令是做啥的。//抢到商品记录一下
jedis.sadd(clientList, clientName+"");
System.out.println("好高兴,顾客:" + clientName + "抢到商品");
break;
}
}
} catch (NumberFormatException e) {
e.printStackTrace();
}finally {
jedis.unwatch();
}
}
}
1.在不成功的情况下,一般需要重试几次,在重试的过程中每次循环都需要重新watch操作,因为每次事务提交之后,watch操作都会失效。
2.在事务提交之后返回的结果对象分为几种情况
1)事务提交前,watch的key发生改变,返回的List对象并不是null,而是一个初始化后的空对象(size==0)
2)事务提交前,watch的key没有改变,事务提交成功,返回的List对象中有一个"OK"的String对象。
如果是高并发场景,就使用乐观锁,因为乐观锁性能比悲观锁好;悲观锁不适合高并发场景。
乐观锁的实现方式:数据库,redis;版本号。
乐观锁的使用场景,悲观锁的使用场景。
synchronized是悲观锁,这种线程一旦得到锁,其他需要锁的线程就挂起的情况就是悲观锁。
CAS操作的就是乐观锁,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。
1、查询时 获取乐观锁的标记
2、只有更新的时候,才会更新锁
3、重试,重新获取锁,然后去更新。
锁机制:乐观锁(版本号,CAS),悲观锁(同步锁)
乐观锁的实现,以及乐观锁的使用场景。 使用DB实现乐观锁。
使用乐观锁进行下单减库存的操作。
4、watch,监视键值对,作用时如果事务提交exec时发现监视的监视对发生变化,事务将被取消。