20191220 使用Redis实现乐观锁

锁机制:

乐观锁: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时发现监视的监视对发生变化,事务将被取消。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章