Redis的事务是一个单独的隔离操作。事务的所有命令都会串行化,按顺序的去执行,事务在执行过程中不会被其他客户端传来的命令打断或影响;redis的事务就是串联多个命令,防止其他命令插队;
Redis事务三特性单独的隔离操作:
- 事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断;
没有隔离级别的概念:
- 队列中的命令没有提交之前都不会实际被执行,因为事务提交前彳出可指令都不会被实际执行;
不保证原子性:
- 事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚;
当输入multi命令,然后再输入其他命令都会进入命令队列(先进先出原则),但是这个时候不会执行,只有输入exce命令后才会执行,而discard命令就是再multi之后,exce命令之前取消队列里面的命令执行的;
如:multi命令开启事务,exec提交事务,当然你可以在exec命令执行前通过discard取消事务提交;
127.0.0.1:6379> multi OK 127.0.0.1:6379> 127.0.0.1:6379> set k1 v1 QUEUED 127.0.0.1:6379> set k2 v2 QUEUED 127.0.0.1:6379> exec 1) OK 2) OK二,异常的事务场景
1,在命令组队时发生异常,那么所有命令都不会执行;
2,命令组队正常,但是事务提交后异常会怎样?
如:下面代码中的k2对应的值不是一个数字字符穿,但是又执行了incr命令,结果除了incr命令不成功外,队列其他命令都成功了;
127.0.0.1:6379> multi OK 127.0.0.1:6379> 127.0.0.1:6379> set k1 aa QUEUED 127.0.0.1:6379> set k2 bb QUEUED 127.0.0.1:6379> set k3 cc QUEUED 127.0.0.1:6379> incr k2 QUEUED 127.0.0.1:6379> set k4 dd QUEUED 127.0.0.1:6379> exec 1) OK 2) OK 3) OK 4) (error) ERR value is not an integer or out of range 5) OK 127.0.0.1:6379> keys * 1) "k4" 2) "k1" 3) "k3" 4) "k2" 127.0.0.1:6379> get k2 "bb"三,redis的乐观锁 WATCH key [key…]
在执行multi之前,先执行watch key1 [key2 , key3] 可以监视一个或者多个key,如果在事务执行之前(exec)这些被监听的key被修改,那么事务将会被打断;
unwatch命令取消对key的监听,当执行watch命令后,执行了exec,discard命令就unwatch则会无效;
四,秒杀案例-超卖问题,redis的事物不一致;需求背景:现在有100个商品需要秒杀卖出,成功秒杀的用户添加到成功清单里面;
1.redis初始化商品:
127.0.0.1:6379> set product:001 100 OK 127.0.0.1:6379> get product:001 "100"
2,秒杀代码编写:
@RequestMapping("/redis/seckill")
public Boolean seckill(){
//商品再redis的key
String productKey = "product:001";
//秒查成功用户再商品中的用户 = user+6位随机数
String user = "user:"+Math.round((Math.random()+1) * 100000);
Jedis jedis = new Jedis("127.0.0.1", 6379);
//redis不存在商品key
String s = jedis.get(productKey);
if(StringUtils.isEmptyOrNull(s)){
System.err.println("秒杀活动未开启!!!");
jedis.close();
return false;
}
if(Integer.parseInt(s)<=0){
System.err.println("商品库存为0,秒杀结束了!!!");
jedis.close();
return false;
}
//商品库存大于0,开始减库存
Long decr = jedis.decr(productKey);
//将秒杀成功的用户写入redis的set集合
jedis.sadd("successUsers",user);
System.out.println("秒杀成功:"+user);
jedis.close();
return true;
}
jmeter模拟并发请求100:
3,查看redis里面的库存情况 偶现负数出现超卖问题;
127.0.0.1:6379> get product:001 "-2" 127.0.0.1:6379>
问题:
这里出现超卖的主要原因就是,代码判断库存是否小于等于0的操作与减库存的操作者之间有空档,就是在这个空档期间造成了超卖;而且上面代码还有问题就是,每次请求都创建了一次连接,极大的造成了资源浪费,更可能因为创建的连接数过多,而造成连接异常错误的抛出;
- 优化点-1: 库存是否为0与减库存操作之间的空档期加事物;
- 优化点-2: redis的连接改为连接池连接;
优化1:jedis连接改连接池:
public class JedisPoolUtil {
private static volatile JedisPool jedisPool;
public static JedisPool getJedisPoolInit(){
if(null==jedisPool){
synchronized (JedisPoolUtil.class){
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(200);
jedisPoolConfig.setMaxIdle(32);
jedisPoolConfig.setMaxWaitMillis(100*1000);
jedisPoolConfig.setBlockWhenExhausted(true);//超过连接是否等待
jedisPoolConfig.setTestOnBorrow(true);//连接时测试一下是否为通路状态 ping:PONG
jedisPool = new JedisPool(jedisPoolConfig,"127.0.0.1",6379,60000);
}
}
return jedisPool;
}
}
优化2:加事务
@RequestMapping("/redis/seckill")
public Boolean seckill(){
//商品再redis的key
String productKey = "product:001";
//秒查成功用户再商品中的用户 = user+6位随机数
String user = "user:"+Math.round((Math.random()+1) * 100000);
//改连接池连接
//Jedis jedis = new Jedis("127.0.0.1", 6379);
JedisPool jedisPoolInit = JedisPoolUtil.getJedisPoolInit();
Jedis jedis = jedisPoolInit.getResource();
//事务优化,对库存加监视
String watch = jedis.watch(productKey);
//redis不存在商品key
String s = jedis.get(productKey);
if(StringUtils.isEmptyOrNull(s)){
System.err.println("秒杀活动未开启!!!");
jedis.close();
return false;
}
if(Integer.parseInt(s)<=0){
System.err.println("商品库存为0,秒杀结束了!!!");
jedis.close();
return false;
}
//商品库存大于0,开始减库存
//Long decr = jedis.decr(productKey);
//jedis.sadd("successUsers",user);
//事务优化,对减库存操作加事务
Transaction multi = jedis.multi();
//组队操作-减库存
multi.decr(productKey);
//组队操作-将秒杀成功的人添加到名单里面
multi.sadd("successUsers",user);
//执行事务,如果监听的库存在这个事务里面有变化那么这个事务将失败
List
测试:
秒杀成功:user:163203 秒杀成功:user:171724 秒杀成功:user:124658 秒杀成功:user:114056 秒杀成功:user:106467 库存变化,事务执行失败,秒杀失败了!!! 库存变化,事务执行失败,秒杀失败了!!! 商品库存为0,秒杀结束了!!! 商品库存为0,秒杀结束了!!! 商品库存为0,秒杀结束了!!! 商品库存为0,秒杀结束了!!! 商品库存为0,秒杀结束了!!!
127.0.0.1:6379> get product:001 "0"
其实到这里还有个问题;增加商品数量为500,请求2000,正常情况下应该时有500个请求能抢到,还有1500个请求抢不到;但是测试发现库存并没有变为0,这是由于redis的乐观锁导致的(redis的乐观锁与mybatis披plus一样是给数据加版本)
127.0.0.1:6379> set product:001 500 OK 127.0.0.1:6379> get product:001 "297"
解决办法:
由于redis是不能使用悲观锁的,我们可以用LUA脚本来实现;将复杂的或者多步的redis操作,写为一个脚本,一次提交给redis执行,减少反复连,接redis的次数。提升性能。LUA脚本是类似redis事务,有厂定的原子性,不会被其他命令插队,可以完成一些redis事务性的操作。但是注意redis的lua脚本功能,只有在Redis 2.6以上的版本才可以使用。利用lua脚本淘汰用户,解决超卖问题。redis 2.6版本以后,通过lua脚本解决争抢问题,实际上是redis利用其单线程的特性,用任务队列的方式解决多任务并发问题。



