Redis相关--事务和锁

一、事务

事务是一个单独的隔离操作:事务中所有的命令都会序列化,顺序地执行。事务在执行过程中,不会被其他客户端发送来的命令所打断。

Redis事务三特性

  • 单独的隔离操作:食物中的所有命令都会序列化,按顺序执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
  • 没有隔离级别的概念:队列中的命令没有提交之前都不会被实际执行
  • 不保证原子性:事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚

1.Redis事务命令

  • Multi 该命令为显示地开启一个事务,使用该命令开启一个事务后,后续的操作命令将会进入事务对应的执行队列,直到执行Exec命令后,入队的操作命令才会按顺序真正执行,该命令执行后客户端会从非事务状态切换为事务状态

  • Watch key [key …] WATCH命令的使用是为了解决 事务并发 产生的不可重复读幻读的问题。该命令为指定监视某个对象,只能在MULTI命令之前执行.如果监视的对象被其他客户端修改,当执行Exec命令时将会放弃事务执行队列中的所有操作命令,并直接向客户端返回(nil)表示事务执行失败。(Watch可以当做Redis乐观锁操作,每次更新数据时都会去判断一下,在此期间是否有人修改过这个数据)

  • Unwatch key [key …] 取消对对象的监视

  • Exec 该命令为正式执行事务,调用该命令后会顺序执行事务队列中的所有操作命令。如果WATCH在Exec命令之前被调用,只有监视中的对象没有被修改,命令才会被执行,否则停止执行。

  • Discard 该命令为清除事务执行队列中的操作命令,并将当前的事务状态改为非事务状态,如果Watch命令在该命令之前被调用,则会释放被Watch命令监视的对象。

image.png
输入Multi开始组队,输入的命令依次进入队列,但不会执行,直到输入Exec命令,组队过程中可以通过Discard来放弃组队

开始组队,开启事务
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
执行
127.0.0.1:6379> exec
1) OK
2) OK
3) OK

2.事务错误处理

  • 组队中某个命令出现了错误,执行时整个队列都会被取消
  • 执行阶段某个命令报错,则只有报错的命令不会执行,其余正常执行,不会回滚。
组队时,有错误命令
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set b1 v1
QUEUED
127.0.0.1:6379> set b2 v1
QUEUED
127.0.0.1:6379> set b3
(error) ERR wrong number of arguments for 'set' command
127.0.0.1:6379> exec
(error) EXECABORT Transaction discarded because of previous errors.
因为v1不是数字,不能+1,所以第一条命令成功 第二条命令失败
127.0.0.1:6379> set c1 v1
QUEUED
127.0.0.1:6379> incr c1
QUEUED
127.0.0.1:6379> exec
1) OK
2) (error) ERR value is not an integer or out of range

3.事务冲突(乐观锁、悲观锁)

示例
三个请求:第一个请求-8000 第二个请求-5000 第三个请求-1000

image.png

悲观锁:每次做操作之前都需要上锁
第一个请求并上锁,扣除8000以后解锁,第二个请求拿到2000并上锁,看到需扣除5000 不够扣,不进行操作

image.png

乐观锁
每次拿数据的时候都认为别人不会进行修改,所以不上锁,但是在更新的时候会判断一下在此期间别人有没有更新这个数据,可使用版本号等机制,乐观锁适用于多读的应用类型。

image.png

4.乐观锁(通过Watch实现)

在执行multi之前,先执行watch key1 【key2】,可以监视一个或多个key,如果在事务执行之前,被监视的key被其他操作改动,那么事务将被打断。
客户端1、客户端2 同事watch balance

客户端1,先exec
127.0.0.1:6379> watch balance
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> incrby balance 10
QUEUED
127.0.0.1:6379> exec
1) (integer) 10010
客户端2,后exec,失败
127.0.0.1:6379>  watch balance
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> incrby balance 20
QUEUED
127.0.0.1:6379> exec
(nil)

二、秒杀案例

image.png

设置10个商品

127.0.0.1:6379> set sk:0101:qt 10
OK
127.0.0.1:6379> keys *
1) "sk:0101:qt"
public class Test {
    public static void main(String[] args) {

        String uid = new Random().nextInt(10000)+"";
        kill(uid,"0101");

    }
    public static boolean kill(String uid,String prodid){
        // uid 和prod-id非空
        if(uid ==null || prodid ==null){
            return false;
        }
        Jedis jedis = new Jedis("101.43.152.64",6379);
        jedis.auth("123456");

        // 拼接key 库存key 秒杀成功用户key
        String kcKey = "sk:"+prodid+":qt";
        String userKey = "sk:" + prodid + ":user";

        // 获取库存,如果库存为null,还没开始
        String s = jedis.get(kcKey);
        if (s ==null){
            System.out.println("秒杀还没开始");
            jedis.close();
            return false;
        }
        // 开始秒杀,判断用户是否重复秒杀
        if (jedis.sismember(userKey, uid)){
            System.out.println("重复秒杀");
            jedis.close();
            return false;
        }
        // 判断如果商品数量=0,表示秒杀结束
        ;
        if (Integer.parseInt(s) == 0){
            System.out.println("秒杀已经结束");
            jedis.close();
            return false;
        }
        // 库存-1,成功者清单+1
        jedis.decr(kcKey);
        jedis.sadd(userKey,uid);
        System.out.println(uid + "秒杀成功");
        jedis.close();
        return false;
    }

}

qt-1 用户+1 \

127.0.0.1:6379> SMEMBERS sk:0101:user
1) "1170"
2) "9167"

047D708A-BF59-436F-820B-28067AB6D216.png

ab工具测试:yum install httpd-tools
1.vim postfile模拟表单提交参数,存放在当前目录
ab -n 1000 -c 100 -p /postfile -T
发现会发生超卖问题

三、超卖问题

39A68BA0-C884-4B8F-BB6F-972C5F00246E.png

增加乐观锁

监视库存 jedis.watch

//使用事务
Transaction multi = jedis.multi();
//组队操作
multi.decr(kcKey);
multi.sadd(userKey,uid);
//执行
List<Object> result = multi.exec();
if (result == null){
    System.out.println("失败了");
}

四、库存遗留问题

乐观锁造成库存遗留问题

五、分布式锁

分布式锁一般需要搭建一个存储服务器,存储锁信息。
1.锁信息必须是会超期超时,不能让一个线程长期占有一个锁导致死锁
2.同一时刻只能有一个线程获取到锁
相关命令
setnx(key,value) “set if not exits”,若kv不存在,则成功设置返回1,否则返回0
get(key) 获取key对应的value,不存在返回null
getset(key,value) 获取key对应的value,不存在返回nil,然后将就得value更新为新的value
expire(key,seconds) 设置kv的有效期为seconds秒
流程图

可靠性

  1. 互斥性。在任意时刻,只有一个客户端能持有锁。
  2. 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
  3. 具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
  4. 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。

加锁

public class RedisTool {
    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";

    /**
     * 尝试获取分布式锁
     *
     * @param jedis      Redis客户端
     * @param lockKey    锁
     * @param requestId  请求标识
     * @param expireTime 超期时间
     * @return 是否获取成功
     */
    public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {

        String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);

        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }
}

可以看到,我们加锁就一行代码:jedis.set(String key, String value, String nxxx, String expx, int time),这个set()方法一共有五个形参:

第一个为key,我们使用key来当锁,因为key是唯一的。

第二个为value,我们传的是requestId,很多童鞋可能不明白,有key作为锁不就够了吗,为什么还要用到value?原因就是我们在上面讲到可靠性时,分布式锁要满足第四个条件解铃还须系铃人,通过给value赋值为requestId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。requestId可以使用UUID.randomUUID().toString()方法生成。

第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作;

第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。

第五个为time,与第四个参数相呼应,代表key的过期时间。

总的来说,执行上面的set()方法就只会导致两种结果:1. 当前没有锁(key不存在),那么就进行加锁操作,并对锁设置个有效期,同时value表示加锁的客户端。2. 已有锁存在,不做任何操作。

解锁操作

/**
 * 释放分布式锁
 * @param jedis Redis客户端
 * @param lockKey 锁
 * @param requestId 请求标识
 * @return 是否释放成功
 */
public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {

    String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));

    if (RELEASE_SUCCESS.equals(result)) {
        return true;
    }
    return false;

}

可以看到,我们解锁只需要两行代码就搞定了!第一行代码,我们写了一个简单的Lua脚本代码,上一次见到这个编程语言还是在《黑客与画家》里,没想到这次居然用上了。第二行代码,我们将Lua代码传到jedis.eval()方法里,并使参数KEYS[1]赋值为lockKey,ARGV[1]赋值为requestId。eval()方法是将Lua代码交给Redis服务端执行。

那么这段Lua代码的功能是什么呢?其实很简单,首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁(解锁)。那么为什么要使用Lua语言来实现呢?因为要确保上述操作是原子性的。关于非原子性会带来什么问题,可以阅读【解锁代码-错误示例2】 。那么为什么执行eval()方法可以确保原子性,源于Redis的特性,下面是官网对eval命令的部分解释:

简单来说,就是在eval命令执行Lua代码的时候,Lua代码将被当成一个命令去执行,并且直到eval命令执行完成,Redis才会执行其他命令。
————————————————
版权声明:本文为CSDN博主「游历三界外不再五行中」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_26977063/article/details/80734343

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值