Redis学习笔记(四)

  • 1,Redis事务的定义
  • 2,Redis事务操作的三个基本命令
  • 3,解决Redis中的事务冲突(乐观锁和悲观锁)
    • 3.1,悲观锁
    • 3.2,乐观锁
    • 3.3,Redis中使用乐观锁
    • 3.4,Redis事务的三特性
  • 4,秒杀案例感受Redis中的事务和锁机制
    • 4.1,秒杀的基本实现
    • 4.2,代码存在的一些问题
    • 4.3,优化一:通过连接池解决连接超时问题
    • 4.4,优化二:通过Redis的事务和锁机制解决超卖问题(重点)
    • *4.5,优化三:通过LUA脚本解决库存遗留问题

1,Redis事务的定义

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

Redis事务的主要作用就是串联多个命令防止别的命令插队。

2,Redis事务操作的三个基本命令

multi、exec、discard为Redis事务操作的三个基本命令

  • 从输入multi命令开始,输入的命令都会依次进入命令队列中,但不会执行。
  • 直到输入exec命令后,Redis会将之前的命令队列中的命令依次执行。
  • 输入multi命令进入组队阶段后,可以使用discard命令放弃组队。

事务执行成功的情况演示如下:




事务执行失败的情况演示如下:

情形一: 组队中某个命令出现了报告错误,执行时整个的所有队列都会被取消。如下图:

情形二: 执行阶段某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚。如下图:

注意:上图中,虽然存在执行失败的命令。但不出错的命令,最终依然可以执行成功!!! 因为Redis数据库不保证原子性。


3,解决Redis中的事务冲突(乐观锁和悲观锁)

引例:

想象一个场景: 你的银行卡里有10000元,共有三个人知道你的银行卡账户和密码:分别是你的前女友、现女友和你自己。三个人同时使用此账户在双十一当天进行消费。(银行卡不可以透支)

  • 前女友存在报复心理,准备花8000元给自己买一台iphone13 pro
  • 现女友想花5000元给自己买一台iphone12
  • 你想花1000元修一修自己使用多年的诺基亚手机


三个人同时消费的时候,如果同时通过了if语句判断。最终卡余额就会变为 -4000。明显存在冲突问题。

以上即为事务冲突问题。

如何处理事务冲突问题?
Redis针对此类问题提供了乐观锁悲观锁的机制进行解决。

3.1,悲观锁

悲观锁: 顾名思义,就是很悲观。每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人就不能拿到这个数据直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等都是在做操作之前先上锁。


悲观锁。前女友操作10000的时候,首先会给余额上锁。上完锁进入阻塞状态,别人就不能操作了,除非锁打开。前女友消费后余额减少8000,剩2000。然后解锁,解锁之后现女友才可以得到这2000,现女友再拿2000进行操作,依然是先上锁。判断2000不能减5000,因此不能操作(当场分手)…----此即为悲观锁机制

悲观锁存在效率低的缺点,因为很多人操作的时候只能一个一个来,不可以同时多人进行操作。

3.2,乐观锁

乐观锁: 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型(很多人读数据但只要少部分人修改),这样可以提高吞吐量。Redis默认就是利用这种机制实现事务的。


乐观锁。对数据操作的时候可以给数据加上一个版本号的字段。一开始所有人都可以得到版本数据,如前女友和现女友都可以得到版本为1.0的数据,此数据均为10000。如果前女友手速较快,就会先消费8000,余额修变为2000,同时版本号也同步更新为1.1。这时如果现女友再进行操作,就需要检查一下当前数据的版本号和数据库中的版本号是否一致。 发现1.0!=1.1,版本号不一致则不能再进行操作,无法消费(当场分手)…—此即为乐观锁机制

乐观锁应用广泛,典型的乐观锁应用场景:12306抢票。当系统中只有一张票的时候所有人都可以参与抢票,但最终只能有一个人支付成功。


3.3,Redis中使用乐观锁

watch key [key …]:在执行multi之前,先执行watch key1 [key2],可以监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。

①连接Redis服务器后在xshell中打开两个终端(分别代表前女友和现女友)

②在终端1中加入数据

③两个终端都监视balance

④两个终端都通过multi开启事务

⑤终端一(前女友)中执行事务

⑥终端二(现女友)中执行事务


以上两个终端(前女友和现女友)都得到balannce(余额)这个数据,对其进行监视(watch),前女友这边先执行就会把balance的版本号进行修改;然后现女友那边经过判断发现版本号发生改变就不能再进行修改。(当场分手)


3.4,Redis事务的三特性

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

4,秒杀案例感受Redis中的事务和锁机制

秒杀是日常中典型的商家活动,接下来我们对其进行模拟。

如果有10个商品需要进行秒杀,100个人参与秒杀。其中一个人抢到了商品,商品库存减一,抢到商品的用户加入到秒杀成功者清单中去。以此类推…


秒杀的大致思路如下:

  • 判断商品id和用户id是否为空,空则return false
  • 连接redis
  • 拼接库存key和秒杀成功用户key(用于区分)
  • 判断库存是否为null。null则说明秒杀未开始
  • 判断用户是否重复秒杀(已经秒杀成功的不可以重复秒杀)
  • 判断商品库存数量是否小于1,大于等于1才可以秒杀
  • 执行秒杀。库存减一,秒杀成功用户的信息添加至清单

4.1,秒杀的基本实现

代码实现如下:

public class SecKill_redis {public static void main(String[] args) {Jedis jedis =new Jedis("47.116.4.200",6379); //此处第一个参数为你的服务器ip地址,第二个参数为你的服务器上redis的端口号jedis.auth("******");  //如果Redis设置了密码此处需要授权System.out.println(jedis.ping()); //测试Redis是否连接成功jedis.close();}//秒杀过程public static boolean doSecKill(String uid,String prodid) throws IOException {//1 uid和prodid非空判断(这两个如果有空值,直接不执行即可)if(uid == null || prodid == null) {return false;}//2 连接redis(通过Jedis进行连接)Jedis jedis = new Jedis("47.116.4.200",6379);jedis.auth("******");  //如果Redis设置了密码此处需要授权//3 拼接相关key(:便于分组;和prodid拼接可以区分不同商品的秒杀过程)// 3.1 库存keyString kcKey = "sk:"+prodid+":qt";// 3.2 秒杀成功用户keyString userKey = "sk:"+prodid+":user";//4 获取库存,如果库存null,表示秒杀还没有开始String kc = jedis.get(kcKey); //库存最终存入了Redis中if(kc == null) {  //表示秒杀未开始System.out.println("秒杀还没有开始,请等待");jedis.close();return false;  //直接返回,不再执行其他操作}// 5 判断用户是否重复秒杀操作(保证一个用户只能秒杀一次)//注意秒杀成功清单中的value值存储秒杀成功者的Id。使用的是set数据类型防止重复。所以此处使用相应的sismember方法取数据if(jedis.sismember(userKey, uid)) {   //sismember方法判断set中是否存在此value值。第一个参数是key,第二个参数为value。System.out.println("已经秒杀成功了,不能重复秒杀");jedis.close();return false;}//6 判断如果商品数量,库存数量小于1,秒杀已经结束if(Integer.parseInt(kc)<1) {   //kc为string类型,需要转换才可判断System.out.println("秒杀已经结束了");jedis.close();return false;}//7 执行秒杀(库存-1,秒杀成功用户添加清单)//7.1 库存-1jedis.decr(kcKey);//7.2 把秒杀成功用户添加清单里面(set集合添加对应sadd方法)jedis.sadd(userKey,uid);System.out.println("秒杀成功了..");jedis.close();return true;}
}

以上代码如果是单个用户进行操作没有问题,但秒杀功能必定涉及多个用户。所以以上代码存在一些并发问题待解决,需要继续进行优化。

4.2,代码存在的一些问题

以上代码如果多个用户的并发操作情况下会出现超卖问题超时问题库存遗留问题

  • 超时:每个操作都要连接Redis,如果有大量请求,Redis不能同时处理,有的请求就需要等待。等待时间过长就会出现连接超时问题
  • 超卖:商品已经秒杀结束了,但还可以秒杀到,导致最终商品数量变为负数。如下图:
  • 库存遗留问题:秒杀结束,但还有库存,此即为库存遗留问题

4.3,优化一:通过连接池解决连接超时问题

连接超时问题可以使用连接池解决。

Jedis为了防止使用连接对象jedis时频繁的创建和销毁,造成资源的浪费,提供了Jedis连接池,可以从连接池中获取Jedis对象,使用完毕后归还这个连接对象。

编写Jedis连接池工具类:(模板如下)

public class JedisPoolUtil {private static volatile JedisPool jedisPool = null;private JedisPoolUtil() {}public static JedisPool getJedisPoolInstance() {   //获取Jedis连接对象if (null == jedisPool) {synchronized (JedisPoolUtil.class) {if (null == jedisPool) {JedisPoolConfig poolConfig = new JedisPoolConfig();poolConfig.setMaxTotal(200);  //最大连接数poolConfig.setMaxIdle(32);   poolConfig.setMaxWaitMillis(100*1000);poolConfig.setBlockWhenExhausted(true);poolConfig.setTestOnBorrow(true);  // ping  PONGjedisPool = new JedisPool(poolConfig, "你的服务器ip", 6379, 60000,"******");  //第二个参数为redis端口号;第三个参数为超时时间;第四个参数为你的redis的密码(设置了Redis密码的话才需要此参数)}}}return jedisPool;}public static void release(JedisPool jedisPool, Jedis jedis) {if (null != jedis) {jedisPool.returnResource(jedis);}}}

有了连接池就可以获取连接,这样就不需要在New的方式创建连接。因此可以将源代码中的new Jedis连接方式进行替换。如下图:

代码替换之后便可以解决连接超时问题。


4.4,优化二:通过Redis的事务和锁机制解决超卖问题(重点)

可以使用乐观锁方式解决超卖问题

乐观锁优化代码体现:


至此完整代码为:

public class SecKill_redis {public static void main(String[] args) {Jedis jedis =new Jedis("47.116.4.200",6379);jedis.auth("******");System.out.println(jedis.ping());jedis.close();}public static boolean doSecKill(String uid,String prodid) throws IOException {//1 uid和prodid非空判断(这两个如果有空值,直接不执行即可)if(uid == null || prodid == null) {return false;}//2 连接redis(通过Jedis进行连接)//通过连接池得到jedis对象JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance();Jedis jedis = jedisPoolInstance.getResource();//3 拼接相关keyString kcKey = "sk:"+prodid+":qt";String userKey = "sk:"+prodid+":user";//监视库存jedis.watch(kcKey);//4 获取库存,如果库存为null,表示秒杀还没有开始String kc = jedis.get(kcKey); if(kc == null) {  System.out.println("秒杀还没有开始,请等待");jedis.close();return false;  //直接返回,不再执行其他操作}// 5 判断用户是否重复秒杀操作(保证一个用户只能秒杀一次)if(jedis.sismember(userKey, uid)) {   System.out.println("已经秒杀成功了,不能重复秒杀");jedis.close();return false;}//6 判断如果商品数量,库存数量小于1,秒杀结已经结束if(Integer.parseInt(kc)<1) {   System.out.println("秒杀已经结束了");jedis.close();return false;}//7 执行秒杀(库存-1,秒杀成功用户添加清单)//使用事务Transaction multi = jedis.multi();   //multi方法开启事务//组队操作multi.decr(kcKey);   //把 decr对库存进行减一的操作 放到命令队列multi.sadd(userKey,uid);   //把 用户信息值加到成功秒杀用户的清单中去的操作 放到命令队列//执行List<Object> results = multi.exec();   //exec方法顺序执行命令队列里的命令。返回的list集合即为最终结果if(results == null || results.size()==0) {System.out.println("秒杀失败了....");jedis.close();return false;}System.out.println("秒杀成功了..");jedis.close();return true;}
}

*4.5,优化三:通过LUA脚本解决库存遗留问题

使用乐观锁可能会造成库存遗留问题:

  • 库存遗留问题:秒杀结束,但还有库存,此即为库存遗留问题

假设有一批用户抢到了商品,其中有一个用户购买成功并修改版本号,版本号修改之后,尽管商品依然存在,但因为版本号不一致了,其他抢到商品的用户就不能继续进行操作了,因此造成库存遗留问题。

Lua 是一个小巧的脚本语言,Lua脚本可以很容易的被C/C++ 代码调用,也可以反过来调用C/C++的函数,Lua并没有提供强大的库,一个完整的Lua解释器不过200k,所以Lua不适合作为开发独立应用程序的语言,而是作为嵌入式脚本语言。

很多应用程序、游戏使用LUA作为自己的嵌入式脚本语言,以此来实现可配置性、可扩展性。因此很多游戏外挂上常见这样的脚本语言。

LUA脚本在Redis中的优势:

  • 将复杂的或者多步的redis操作,写为一个脚本,一次提交给redis执行,减少反复连接redis的次数。提升性能。
  • LUA脚本是类似redis事务,有一定的原子性,不会被其他命令插队,可以完成一些redis事务性的操作。

注意:redis的lua脚本功能,只有在Redis 2.6以上的版本才可以使用。

redis 2.6版本以后,通过lua脚本解决争抢问题,实际上是redis 利用其单线程的特性,用任务队列的方式解决多任务并发问题。

LUA脚本如下(了解):

local userid=KEYS[1];  此处定义变量
local prodid=KEYS[2];
local qtkey="sk:"..prodid..":qt";     相当于拼接key
local usersKey="sk:"..prodid.":usr';
local userExists=redis.call("sismember",usersKey,userid);  调用redis中的sismember命令
if tonumber(userExists)==1 then return 2;  约定2代表秒杀过,不能再进行秒杀了
end
local num= redis.call("get" ,qtkey);   调用get方法
if tonumber(num)<=0 then return 0;  秒杀结束
else redis.call("decr",qtkey); 库存减一redis.call("sadd",usersKey,userid);  添加用户至清单
end
return 1;

把此脚本加入Java代码中:

public class SecKill_redisByScript {private static final  org.slf4j.Logger logger =LoggerFactory.getLogger(SecKill_redisByScript.class) ;public static void main(String[] args) {JedisPool jedispool =  JedisPoolUtil.getJedisPoolInstance();Jedis jedis=jedispool.getResource();System.out.println(jedis.ping());Set<HostAndPort> set=new HashSet<HostAndPort>();// doSecKill("201","sk:0101");}static String secKillScript ="local userid=KEYS[1];\r\n" +          //lua脚本"local prodid=KEYS[2];\r\n" + "local qtkey='sk:'..prodid..\":qt\";\r\n" + "local usersKey='sk:'..prodid..\":usr\";\r\n" + "local userExists=redis.call(\"sismember\",usersKey,userid);\r\n" + "if tonumber(userExists)==1 then \r\n" + "   return 2;\r\n" + "end\r\n" + "local num= redis.call(\"get\" ,qtkey);\r\n" + "if tonumber(num)<=0 then \r\n" + "   return 0;\r\n" + "else \r\n" + "   redis.call(\"decr\",qtkey);\r\n" + "   redis.call(\"sadd\",usersKey,userid);\r\n" + "end\r\n" + "return 1" ;static String secKillScript2 = "local userExists=redis.call(\"sismember\",\"{sk}:0101:usr\",userid);\r\n" +" return 1";public static boolean doSecKill(String uid,String prodid) throws IOException {//通过连接池获取Jedis连接JedisPool jedispool =  JedisPoolUtil.getJedisPoolInstance();Jedis jedis=jedispool.getResource();//String sha1=  .secKillScript;String sha1=  jedis.scriptLoad(secKillScript);    //加载脚本Object result= jedis.evalsha(sha1, 2, uid,prodid);String reString=String.valueOf(result);if ("0".equals( reString )  ) {System.err.println("已抢空!!");}else if("1".equals( reString )  )  {System.out.println("抢购成功!!!!");}else if("2".equals( reString )  )  {System.err.println("该用户已抢过!!");}else{System.err.println("抢购异常!!");}jedis.close();return true;}
}

至此优化完成,程序可以很好地支持并发操作。

Redis的事务和锁机制(乐观锁和悲观锁)相关推荐

  1. mysql悲观锁和乐观区别_MySQL悲观锁和乐观锁的区别是什么

    MySQL悲观锁和乐观锁的区别是什么 区别如下: 1.概念不同 乐观锁( Optimistic Locking): 顾名思义,对加锁持有一种乐观的态度,即先进行业务操作,不到最后一步不进行加锁,&qu ...

  2. Java中的锁机制 -- 乐观锁、悲观锁、自旋锁、可重入锁、读写锁、公平锁、非公平锁、共享锁、独占锁、重量级锁、轻量级锁、偏向锁、分段锁、互斥锁、同步锁、死锁、锁粗化、锁消除

    文章目录 1. Java中的锁机制 1.1 乐观锁 1.2 悲观锁 1.3 自旋锁 1.4 可重入锁(递归锁) 1.5 读写锁 1.6 公平锁 1.7 非公平锁 1.8 共享锁 1.9 独占锁 1.1 ...

  3. 【Java锁体系】一、悲观锁和乐观锁

    目录: [Java锁体系]一.悲观锁和乐观锁 [Java锁体系]二.Java中的CAS机制算法 [Java锁体系]三.自旋锁详解 [Java锁体系]四.Synchronized关键字详解 [Java锁 ...

  4. mysql行锁索引问题_Mysql锁机制--索引失效导致行锁变表锁

    =============== Tips:在阅读本文前,最好先阅读 这篇(Mysql锁机制--行锁)文章~ 在上篇文章中,我们看到InnoDB默认的行锁可以使得操作不同行时不会产生相互影响.不会阻塞, ...

  5. 【Redis】事物和锁机制乐观锁悲观锁

    目录 1. Redis 的事务定义 2. Multi.Exec.discard 3. 事务的错误处理 4. 事务冲突的问题 悲观锁 乐观锁 1. Redis 的事务定义 Redis 事务是一个单独的隔 ...

  6. mysql锁机制——乐观锁、悲观锁;共享锁、排他锁、行表锁、间隔后码锁、MVCC 与 thinkphp的lock解析

    锁的引入 如果A有100元,同时对B.C转账,若处理是同时的,则此时同时读取A的余额为100元,在对两人转账后写回,A的余额不是0元而是50元.因此,为了防止这种现象的出现,要引入锁的概念,如只有在A ...

  7. JDBC(本质,配置环境变量,JDBC编程六步,类加载注册,sql注入,事务问题,封装工具类,悲观锁,乐观锁)

    JDBC 2021.5.21 依然跟着动力节点杜老师学!!! 1.什么是JDBC? Java DataBase Connectivity 在java语言中编写sql语句,对mysql数据库中的数据进行 ...

  8. mysql默认锁机制是什么_MySQL中锁机制的原理是什么

    MySQL中锁机制的原理是什么 发布时间:2020-12-08 14:48:30 来源:亿速云 阅读:81 作者:Leah MySQL中锁机制的原理是什么?针对这个问题,这篇文章详细介绍了相对应的分析 ...

  9. mysql锁总结知乎_Mysql悲观锁乐观锁区别与使用场景

    概念上区别 乐观锁(Optimistic Locking):顾名思义,对加锁持有一种乐观的态度,即先进行业务操作,不到最后一步不进行加锁,"乐观"的认为加锁一定会成功的,在最后一步 ...

  10. 11. mysql锁机制_深入探讨MySQL锁机制

    MySQL锁机制究竟是怎样的呢?这是很多人都提到过的问题,下面就为您详细介绍MySQL锁机制方面的知识,希望可以让您MySQL锁机制有更多的了解. 当前MySQL已经支持 ISAM, MyISAM, ...

最新文章

  1. Gartner:人工智能将改变个人设备领域的游戏规则
  2. discuz mysql data_Discuz!显示 Database Error的原因和解决方法
  3. 关于ListView的作业
  4. MapReduce入门和优化方案
  5. php如何每天自调用不同的ccs,Python-ccs高级选择器 盒模型
  6. maven安装本地jar到本地仓库
  7. c语言常见输入输出格式简单介绍
  8. 单项选择题标准化考试系统设计c语言版
  9. 微信小程序蓝牙通讯、串口通讯、调试助手(HC-08等 )
  10. 打开php页面变成下载的解决办法
  11. 大白菜装机教程win10_大白菜U盘启动工具|大白菜超级U盘启动制作工具 V6.0_2009.25官方版下载...
  12. python实现的EDF(earliest deadline first)算法
  13. win10系统要求配置_观察者系统还原游戏配置要求高吗?Observer: System Redux硬件一览!...
  14. 移动开发大作业————随手记(主界面和编辑界面)
  15. Android Studio 连接第三方模拟器
  16. 数据库实现计算工作日时间差--去除节假日及周末
  17. ACCESS数据库窗体
  18. win10局域网中设置共享文件夹
  19. 安装Hadoop,让word count飞起来
  20. BUPT 离散下期末复习(在考试结束前不断迭代)

热门文章

  1. 【leetcode】551. 学生出勤记录 I(student-attendance-record-i)(模拟)[简单]
  2. K临近算法检测异常操作(一)
  3. 神器sublime2配置xdebug调试PHP
  4. Disk Xray for Mac v2.8.1 重复文件及系统垃圾清理
  5. GreenAMP下载:可将Apache MySQL PHP安装成绿色软件
  6. [Maven] The Super POM
  7. linux 执行sh脚本传参数
  8. 西安石油大学2023年第三届里奇杯编程大赛(初赛)
  9. 在 Linux 中安装 Thunderbird
  10. 股票期权平台交易如何防止被骗?