一、概述
redis事务是一个单独的隔离操作,事务中所有命令都会序列化、按顺序执行。事务执行过程中不会被其他客户端发来的命令请求打断,事务主要作用就是串联多个命令防止别的命令插队
二、事务执行命令
redis和mysql事务有本质区别,redis从输入Multi命令开始,输入的命令都会依次进入命令队列中,但不会执行,直到输入了执行过程Exec后,redis会将之前的命令队列中的命令依次执行,组队的过程中可以通过discard放弃组队,如下图演示
演示对列
演示放弃组队
三、事务异常错误
第一种错误:组队阶段某个命令发生错误,执行时整个的队列都会被取消
第二种错误:执行时队列中的某个名列发生错误,只有发生错误的不会被执行,不会回滚,其他正常执行
四、事务冲突和处理
1、事务冲突概述
马上618了、假如一个场景:有很多人有你的账户并且该账户只有10000,同时去参加618抢购,同时发送了三个请求
一个请求想给金额减8000
一个请求想给金额减5000
一个请求想给金额减1000
这三个请求同时发出,假如都执行成功了,那么此时账户的余额就变成了10000-8000-5000-1000 = -4000,此时账户余额变成负的了,显然这在实际生活中是很不合理的。
这时我们能想到的就是加锁了。
2、解决事务冲突
2.1、悲观锁
顾名思义,就是很悲观的那种,喜欢胡思乱想每次去拿数据的时候总认为别人会改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
2. 2、乐观锁
和悲观锁相反在拿到数据时不会加锁,因为他相信这世界是美好的,别人不会修改,但出于严谨还是会在改数据的那一刻会检查一下有没有被人修改。可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种check-and-set机制实现事务的。
乐观锁比悲观锁的效率要高,因为悲观锁每次都会加锁而乐观锁只是在修改数据时检查一下版本号是不是刚拿到的版本号不是就不能执行,是就执行。
3、redis中WATCH
在执行multi之前,先执行watch key1 [key2],可以监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。
实例:
设置一个键为balance 值为10的字符串类型数据。
使用watch监控
开启两个客户端,分别监控后再开启事务对balance进行+100操作,客户端一先提交会成功,客户端二提交会失败,这就是WAYCH对事务监控,防止事务冲突。
客户端一:执行事务成功
客户端二:执行事务失败
总结
在实际应用中很容易出现并发问题、所以我们要在redis事务中加锁解决事务的冲突问题
在redis中使用的check-and-set乐观锁机制实现事务的
redis中的乐观锁使用watch命令来实现的
4、redis事务锁机制案例---秒杀
4.1、概述
一个秒杀主要由两个操作商品数量减少和记录秒杀成功的用户
/** * 秒杀成功是否 * @param uid 秒杀用户 * @param pid 商品id * @return 秒杀成功 */ public Boolean doKill(String uid,Integer pid){ //1.uid pid非空判断 if(uid == null || pid == null){ return false; } //2.连接jedis Jedis jedis = new Jedis("127.0.0.1","6379"); //3.拼接key //库存key String kcKey = "kc"+pid; //用户key String userKey = "user"+pid; //4.判断库存是否为空 String kc = jedis.get(kcKey; if(kc == null){ jedis.close(); return false; } //5.判断用户重复秒杀 if(jedis.sismember(userKey,uid)){ jedis.close(); return false; } //6.库存大于0 if(Integer.parseInt(kc)<=0){ jedis.close(); return false; } //7.秒杀 jedis.decr(kcKey); jedis.sadd(userKey,uid);//用户id值时set类型的 jedis.close(); return true; }
4.2、并发问题
使用工具ab模拟并发或者jmeter模拟请求时出现两个问题
1、连接超时(大量请求redis导致阻塞)
解决方式:引入连接池,如下自定义一个连接池工具类
import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; public class JedisPoolUtils { private static volatile JedisPool jedisPool = null; private JedisPoolUtils(){ } //懒汉式双重校验锁,线程安全 public static JedisPool getJedisPoolInstance() { if(null == jedisPool) { synchronized (JedisPoolUtils.class) { if(null == jedisPool) { JedisPoolConfig poolConfig = new JedisPoolConfig(); poolConfig.setMaxTotal(200); poolConfig.setMaxIdle(32); poolConfig.setMaxWaitMillis(100*1000); poolConfig.setTestOnBorrow(true); jedisPool = new JedisPool(poolConfig,"192.168.235.128",6379); } } } return jedisPool; } public static void release(JedisPool jedisPool, Jedis jedis) { if(null != jedis) { jedisPool.returnResourceObject(jedis); } } }
上面秒杀代码更改
Jedis jedis = new Jedis("127.0.0.1","6379");
更改
2、并发错误问题,库存变为负值
通过乐观锁解决,更改上面代码加上监视和事务
4.3、库存遗留问题
通过上面可以解决超卖问题,但是会出现库存 剩余问题,因为乐观锁是通过修改该版本来控制,比如第10个用户监视完之后版本改为1 后面改的用户版本不一致不会操作所以剩余
解决方法,通过lua脚本解决
lua概述
lua是将复杂的或者多步的redis操作写成一个脚本,一次提交给redis执行,减少反复连接redis的次数提升性能,lua脚本类似redis事务,有一定的原子性,不会被其他命令插队,可以完成一些redis事务性的操作,但是redis的lua脚本只有在redis2.6以上版本才能使用,利用lua脚本解决超卖问题,实际上是利用redis单线程特性用任务队列解决任务并发问题
实现类
4.4、扩展
对于秒杀操作spring boot ,redis template也可以实现
/** * 通过redis 事务 实现的秒杀 * @param skuCode 商品编码 * @param buyNum 购买数量 * @return 购买数量 */ @Service public class GoodsServiceImpl { @Autowired StringRedisTemplate redisTemplate; //redis对任何不合法的值,都称为ERR。用RedisTemplate 序列化的数字不能转化 //使用GenericToStringSerializer、StringRedisSerializer序列化器,都可以使用increment方法 保存的数字是 10 而不是"10" 字符串 /* 1 redisTemplate.excute(SessionCallback sessionCallback) 是执行事务的api * 2 所以要实现SessionCallback 来实现redis 事务。 * 3 如果直接 通过redisTemplate 执行事务命令 会报错*/ public Long flashSellByRedisWatch(String skuCode,int num){ SessionCallback<Long> sessionCallback = new SessionCallback<Long>() { @SuppressWarnings("unchecked") @Override public Long execute(RedisOperations operations) throws DataAccessException { int result = num; //redis 乐观锁 operations.watch(skuCode); ValueOperations<String, String> valueOperations = operations.opsForValue(); String goodNumStr = valueOperations.get(skuCode); Integer goodNum = Integer.valueOf(goodNumStr); //标记一个事务块的开始。 //事务块内的多条命令会按照先后顺序被放进一个队列当中, //最后由 EXEC 命令原子性(atomic)地执行。 operations.multi(); if (goodNum>=num) { valueOperations.increment(skuCode, 0-num);//负数就是需要减少几个商品数量 }else{ result = 0; } //多条命令执行的结果集合 List exec = operations.exec(); if(exec.size()>0){ System.out.println("执行成功: "+exec); } return (long) result; } }; return redisTemplate.execute(sessionCallback); } }