一、事务
事务是一个单独的隔离操作:事务中所有的命令都会序列化,顺序地执行。事务在执行过程中,不会被其他客户端发送来的命令所打断。
Redis事务三特性
- 单独的隔离操作:食物中的所有命令都会序列化,按顺序执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
- 没有隔离级别的概念:队列中的命令没有提交之前都不会被实际执行
- 不保证原子性:事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚
1.Redis事务命令
-
Multi 该命令为显示地开启一个事务,使用该命令开启一个事务后,后续的操作命令将会进入事务对应的执行队列,直到执行Exec命令后,入队的操作命令才会按顺序真正执行,该命令执行后客户端会从
非事务状态切换为事务状态
。 -
Watch key [key …]
WATCH
命令的使用是为了解决事务并发
产生的不可重复读
和幻读
的问题。该命令为指定监视某个对象,只能在MULTI命令之前执行.如果监视的对象被其他客户端修改,当执行Exec命令时将会放弃事务执行队列中的所有操作命令,并直接向客户端返回(nil)表示事务执行失败。(Watch可以当做Redis乐观锁操作,每次更新数据时都会去判断一下,在此期间是否有人修改过这个数据)
-
Unwatch key [key …] 取消对对象的监视
-
Exec 该命令为正式执行事务,调用该命令后会顺序执行事务队列中的所有操作命令。如果WATCH在Exec命令之前被调用,只有监视中的对象没有被修改,命令才会被执行,否则停止执行。
-
Discard 该命令为清除事务执行队列中的操作命令,并将当前的事务状态改为非事务状态,如果Watch命令在该命令之前被调用,则会释放被Watch命令监视的对象。
输入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
悲观锁:每次做操作之前都需要上锁
第一个请求并上锁,扣除8000以后解锁,第二个请求拿到2000并上锁,看到需扣除5000 不够扣,不进行操作
乐观锁
每次拿数据的时候都认为别人不会进行修改,所以不上锁,但是在更新的时候会判断一下在此期间别人有没有更新这个数据,可使用版本号等机制,乐观锁适用于多读的应用类型。
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)
二、秒杀案例
设置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"
ab工具测试:yum install httpd-tools
1.vim postfile模拟表单提交参数,存放在当前目录
ab -n 1000 -c 100 -p /postfile -T
发现会发生超卖问题
三、超卖问题
增加乐观锁
监视库存 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秒
流程图
可靠性
- 互斥性。在任意时刻,只有一个客户端能持有锁。
- 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
- 具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
- 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
加锁
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