事务的操作multi、exec、discard:
开启事务multi
设定事务的开启位置,此指令执行后,后续的所有指令均加入到事务中
执行事务exec
设定事务的结束位置,同时执行事务。与multi成对出现,成对使用
注意:加入事务的命令暂时进入到任务队列中,并没有立即执行,只有执行exec命令才开始执行
取消事务discard
终止当前事务的定义,发生在multi之后,exec之前
即:从输入Multi命令开始,输入的命令都会依次进入命令队列中,但不会执行,直到输入Exec后,Redis会将之前的命令队列中的命令依次执行。组队的过程中可以通过discard来放弃组队。
事务的错误处理:
组队中某个命令出现了报告错误,执行时整个的所有队列都会被取消。
如果执行阶段某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚。
watch锁是一种乐观锁的概念:
乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种check-and-set机制实现事务的。
watch
在执行multi之前,先执行watch key1 [key2],可以监视一个(或多个) key ,如果在执行exec前如果key被别的线程操作了,则终止事务执行。
unwatch
取消 WATCH 命令对所有 key 的监视。
如果在执行 WATCH 命令之后,EXEC 命令或DISCARD 命令先被执行了的话,那么就不需要再执行UNWATCH 了。
Redis中记录商品的库存数量和秒杀成功者清单
存在的问题:
使用
事务(乐观锁)
解决
解决:使用
连接池
jedis连接资源的创建与销毁是很消耗程序性能,所以jedis为我们提供了
jedis的池化技术
,jedisPool在创建时初始化一些连接资源存储到连接池中,使用jedis连接资源时不需要创建,而是从连接池中获取一个资源进行redis的操作,使用完毕后,不需要销毁该jedis连接资源,而是将该资源归还给连接池,供其他请求使用。
常用参数:
MaxTotal
:控制一个pool可分配多少个jedis实例,通过pool.getResource()来获取;如果赋值为-1,则表示不限制;如果pool已经分配了MaxTotal个jedis实例,则此时pool的状态为exhausted。maxIdle
:控制一个pool最多有多少个状态为idle(空闲)的jedis实例;MaxWaitMillis
:表示当borrow一个jedis实例时,最大的等待毫秒数,如果超过等待时间,则直接抛JedisConnectionException;testOnBorrow
:获得一个jedis实例的时候是否检查连接可用性(ping());如果为true,则得到的jedis实例均是可用的;
代码示例
Redis连接池代码:
import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; public class JedisPoolUtil { private static volatile JedisPool jedisPool = null; private JedisPoolUtil() { } public static JedisPool getJedisPoolInstance() { 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 PONG jedisPool = new JedisPool(poolConfig, "192.168.2.4", 6379, 60000 ); } } } return jedisPool; } public static void release(JedisPool jedisPool, Jedis jedis) { if (null != jedis) { jedisPool.returnResource(jedis); } } }
Servlet代码:
public class SecKillServlet extends HttpServlet { private static final long serialVersionUID = 1L; public SecKillServlet() { super(); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String userid = new Random().nextInt(50000) +"" ; String prodid = req.getParameter("prodid"); boolean isSuccess=SecKill_redis.doSecKill(userid,prodid); // boolean isSuccess= SecKill_redisByScript.doSecKill(userid,prodid); resp.getWriter().print(isSuccess); } }
Redis操作代码:
public class SecKill_redis { //秒杀过程 public static boolean doSecKill(String uid,String prodid) throws IOException { //1 uid和prodid非空判断 if(uid == null || prodid == null) { return false; } //2 连接redis //通过连接池得到jedis对象 JedisPool jedisPool = JedisPoolUtil.getJedisPoolInstance(); Jedis jedis = jedisPool.getResource(); //3 拼接key // 3.1 库存key String kcKey = "sk:"+prodid+":qt"; // 3.2 秒杀成功用户key 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 秒杀过程 //使用事务 Transaction multi = jedis.multi(); //组队操作 //7.1 库存-1 multi.decr(kcKey); //7.2 把秒杀成功用户添加清单里面 multi.sadd(userKey,uid); //执行 List<Object> results = multi.exec(); if (results == null || results.size() == 0) { System.out.println("秒杀失败了...."); jedis.close(); return false; } System.out.println("秒杀成功了.."); jedis.close(); return true; } }
前端发起请求
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Insert title here</title> </head> <body> <h1>iPhone 13 Pro !!! 1元秒杀!!! </h1> <form id="msform" action="${pageContext.request.contextPath}/doseckill" enctype="application/x-www-form-urlencoded"> <input type="hidden" id="prodid" name="prodid" value="0101"> <input type="button" id="miaosha_btn" name="seckill_btn" value="秒杀点我"/> </form> </body> <script type="text/javascript" src="${pageContext.request.contextPath}/script/jquery/jquery-3.1.0.js"></script> <script type="text/javascript"> $(function(){ $("#miaosha_btn").click(function(){ var url=$("#msform").attr("action"); $.post(url,$("#msform").serialize(),function(data){ if(data=="false"){ alert("抢光了" ); $("#miaosha_btn").attr("disabled",true); } } ); }) }) </script> </html>
问题:已经秒光,可是还有库存。原因,就是乐观锁导致很多请求都失败。先点的没秒到,后点的可能秒到了。
解决:
将复杂的或者多步的redis操作,写为一 个脚本,一次提交给redis执行,减少反复连接redis的次数。提升性能。
LUA脚本是类似redis事务,有一定的原子性,不会被其他命令插队,可以完成一些redis事务性的操作。
但是注意redis的lua脚本功能,只有在Redis 2.6以上的版本才可以使用。
通过lua脚本解决争抢问题,实际上是redis 利用其单线程的特性,用任务队列的方式解决多任务并发问题。
LUA脚本
local userid=KEYS[1]; local prodid=KEYS[2]; local qtkey="sk:"..prodid..":qt"; local usersKey="sk:"..prodid.":usr'; local userExists=redis.call("sismember",usersKey,userid); if tonumber(userExists)==1 then return 2; end local num= redis.call("get" ,qtkey); if tonumber(num)<=0 then return 0; else redis.call("decr",qtkey); redis.call("sadd",usersKey,userid); end return 1;
Redis使用LUA脚本代码
public class SecKill_redisByScript { private static final org.slf4j.Logger logger =LoggerFactory.getLogger(SecKill_redisByScript.class) ; static String secKillScript ="local userid=KEYS[1];\r\n" + "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 { // JedisPool jedispool = JedisPoolUtil.getJedisPoolInstance(); // Jedis jedis=jedispool.getResource(); Jedis jedis = new Jedis("192.168.2.4", 6379); //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; } }
CentOS6 默认安装
CentOS7需要手动安装
安装ab工具:yum install httpd-tools
执行代码:
ab -n 2000 -c 200 -k -p ~/postfile -T application/x-www-form-urlencoded http://192.168.81.1:8080/seckill/doseckill -n 连接数 -c 并发数
Redis
的高性能
是由于其将所有数据
都存储
在了内存
中,为了使Redis在重启之后仍能保证数据不丢失
,需要将数据从内存中同步到硬盘中,这一过程就是持久化
。Redis支持两种方式的持久化,一种是 RDB方式
,一种是 AOF方式
。可以单独使用其中一种或将二者结合使用。
RDB持久化
(默认支持,无需配置)指定的时间间隔
内将内存中的数据集快照
写入磁盘。快照(Snapshot)
也称为RDB持久化方式AOF持久化
日志
的形式记录服务器所处理的每一个写操作
,在Redis服务器启动之初会读取该文件来重新构建redis数据库,以保证启动后数据库中的数据
是完整的。redis可以同时使用RDB和AOF
是什么?
在指定的时间间隔
内将内存中的数据集快照
写入磁盘, 也就是行话讲的Snapshot快照,它恢复时是将快照文件直接读到内存里
备份是如何执行的?
Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到 一个临时文件
中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。 整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能 如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失。
Fork?
Fork的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、程序计数器等) 数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程
在Linux程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,出于效率考虑,Linux中引入了“写时复制技术”
一般情况父进程和子进程会共用同一段物理内存,只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。
BGSAVE
和 SAVE
指令bgsave命令:
BGSAVE命令
来创建一个快照
,当redis服务器
接收到客户端
的BGSAVE
命令时,redis会调用fork
来创建一个子进程
,然后子进程
负责将快照写入磁盘中,而父进程则继续处理命令请求。即:Redis会在后台异步进行快照操作, 快照同时还可以响应客户端请求
。save命令:
SAVE命令
来创建一个快照
,接收到SAVE命令的redis服务器在快照创建完毕之前将不再响应任何其他的命令。即:使用SAVE命令在快照创建完毕之前,redis处于阻塞状态
,无法对外服务(写操作)如果用户在redis.conf
中设置了save配置选项
,redis会在save选项条件满足之后自动触发一次BGSAVE命令, 如果设置多个save配置选项,当任意一个save配置选项条件满足,redis也会触发一次BGSAVE命令
表示 900S(15分钟)
, key发生1次变化, 就触发一次 bgsave命令, 持久化一次
表示300S(5分钟)
, key发生10次变化, 就触发一次bgsave命令, 持久化一次
表示60S(1分钟)
, key发生10000次变化, 就触发一次bgsave命令, 持久化一次
上面自动触发的规则: 标明key改变的越频繁, 触发快照持久化到硬盘的时间就越短;
redis服务器
接收到redis客户端
发来的shutdown指令
关闭服务器时,会执行一个save命令
,阻塞所有的客户端,不再执行客户端执行发送的任何命令,并且在save命令执行完毕之后关闭服务器rdb文件名
在redis.conf中配置文件名称,默认为dump.rdb
配置文件位置
rdb文件的保存路径,也可以修改。默认为Redis启动时命令行所在的目录下
dir “/myredis/”
stop-writes-on-bgsave-error
后台存储过程中如果出现错误现象,是否停止保存操作。推荐yes.
rdbcompression 压缩文件
对于存储到磁盘中的快照,可以设置是否进行压缩存储。如果是的话,redis会采用LZF算法进行压缩。如果你不想消耗CPU来进行压缩的话,可以设置为关闭此功能,但会使存储的文件变大(巨大)。推荐yes.
rdbchecksum 检查完整性
在存储快照后,还可以让redis使用CRC64算法来进行数据校验,但是这样做会增加大约10%的性能消耗,如果希望获取到最大的性能提升,可以关闭此功能。推荐yes.
cp dump2.rdb dump.rdb
优点:
应用:服务器中每X小时执行bgsave备份,并将RDB文件拷贝到远程机器中,用于灾难恢复
缺点:
AOF持久化
可以将所有客户端
执行的(set)写命令
记录到日志文件
中,即会将被执行的写命令写到AOF的文件末尾,以此来记录数据发生的变化
AOF文件的记录的数据集
.(将写命令原封不动的写到aof的文件, 下次再从新执行就恢复了数据)日志
的形式记录服务器所处理的每一个写操作
,在Redis服务器启动之初会读取该文件来重新构建redis数据库,以保证启动后数据库中的数据
是完整的。AOF持久化流程
开启AOF持久化:
在redis的默认配置(redis.conf)
中AOF持久化机制
是没有开启
的,需要在配置中开启。
appendonly yes
开启持久化appendfilename "appendonly.aof"
指定生成文件名称注意:AOF和RDB同时开启,系统默认取AOF的数据(数据不会存在丢失)
AOF的恢复:
备份和恢复的操作同RDB一样,都是拷贝备份文件,将有数据的aof文件复制一份保存到对应目录。重启redis然后redis自动加载aof文件
。
AOF的修复:
如遇到AOF文件损坏,通过/usr/local/bin/redis-check-aof--fix appendonly.aof
进行恢复
修改appendfsync everysec|always|no
指定
关键字 | 持久化时机 | 解释 |
---|---|---|
appendfsync | always | 始终同步,每次Redis的写入都会立刻记入日志;性能较差但数据完整性比较好 |
appendfsync | everysec | 每秒将缓冲区中的指令同步到AOF文件中,数据准确性较高,性能较高,建议使用,也是默认配置。在系统突然宕机的情况下丢失1秒内的数据 |
appendfsync | no | 由操作系统控制每次同步到AOF文件的周期,整体过程不可控 |
随着命令不断写入AOF,文件会越来越大,为了解决这个问题,Redis引入了AOF重写机制压缩文件体积。AOF文件重写是将Redis进程内的数据转化为写命令同步到新AOF文件的过程。简单说就是将对同一个数据的若干个条命令执行结果转化成最终结果数据对应的指令进行记录。可以使用命令bgrewriteaof
。
触发重写方式
1、客户端方式触发重写bgrewriteaof
命令(手动重写)
BGREWRITEAOF命令
不会阻塞redis的服务2、服务器配置方式自动触发重写aof
redis.conf
中的auto-aof-rewrite-percentage
选项 , 下图。auto-aof-rewrite-percentage值为100
和auto-aof-rewrite-min-size 64mb
,并且启用的AOF持久化时
,那么当AOF文件体积大于64M,并且AOF文件的体积比上一次重写之后体积大了至少一倍(100%)时,会自动触发,如果重写过于频繁,用户可以考虑将auto-aof-rewrite-percentage设置为更大重写流程:
重写aof文件
的操作,并没有读取旧的aof文件
,而是将整个内存中的数据库内容用命令的方式重写了一个新的aof文件, 替换原有的文件这点和快照有点类似。步骤:
fork
,现在有父子两个进程, 子进程
根据内存中的数据库快照
,往临时文件中写入重建数据库状态的命令对数据非常敏感,建议使用默认的AOF持久化方案
数据呈现阶段有效性,建议使用RDB持久化方案
综合比对
背景:为了避免单点Redis服务器故障,准备多台服务器,互相连通。将数据复制多个副本保存在不同的服
务器上,连接在一起,并保证数据是同步的。即使有其中一台服务器宕机,其他服务器依然可以继续
提供服务,实现Redis的高可用,同时实现数据冗余备份。
是什么?
主从复制即将master中的数据即时、有效的复制到slave中。master进行写数据,执行写操作时,将出现变化的数据自动同步到slave;slave进行读数据,写数据被禁止。
特征:
一个master可以拥有多个slave,一个slave只对应一个master
作用:
读写分离
:master写、slave读,提高服务器的读写负载能力负载均衡
:基于主从结构
,配合读写分离
,由slave分担master负载,并根据需求的变化,改变slave的数量,通过多个从节点分担数据读取负载,大大提高Redis服务器并发量与数据吞吐量
故障恢复
:当master出现问题
时,由slave
提供服务,实现快速的故障恢复数据冗余
:实现数据热备份,是持久化之外的一种数据冗余方式, 因为slave中和master数据是同步
的高可用基石
:基于主从复制
,构建哨兵模式
与集群
,实现Redis的高可用
方案需要注意,主从复制的开启,完全是在从节点发起的;不需要我们在主节点做任何事情。
从节点开启主从复制,有3种方式:
(1)配置文件
在从服务器的配置文件中加入:slaveof <masterip> <masterport>
(2)启动命令
redis-server启动命令后加入 --slaveof <masterip> <masterport>
(3)客户端命令
Redis服务器启动后,直接通过客户端执行命令:slaveof <masterip> <masterport>,则该Redis实例成为从节点。
上述3种方式是等效的,下面以客户端命令的方式为例,看一下当执行了slaveof后,Redis主节点和从节点的变化。
新建多个redis的配置文件
注意:配置文件中Appendonly关掉或者换名字(这里使用rdb持久化为例)
可以在配置文件中设置 slave-priority 数字
。设置从机的优先级,值越小,优先级越高,用于选举主机时使用。默认100
启动三台redis服务器
查看系统进程,看看三台服务器是否启动
进行从服务器的配置,并查看三台主机运行情况
在6380和6381上执行: slaveof 127.0.0.1 6379
,配置6380服务器和6381服务器为6379的从服务器。
查看主服务的配置信息。通过info Replication
执行结果
slaveof no one
主动断开。需要注意的是,从节点断开复制后,不会删除已有的数据,只是不再接受主节点新的数据变化。注意:
当一个从服务器挂掉之后,重启之后是一个主服务器。如果想成为之前的从服务器,还需要重新配置。且重新配置之后数据和主服务器相同。
当主服务器挂掉之后,从服务器不做任何事情。而主服务器重启之后,仍然是主服务器,其之前的从服务器依然存在。
薪火相传模式即:上一个slave(从机)是下一个slave(从机)的Master(主机)
优点: 从机同样可以接收其他从机的连接和同步请求,那么该从机作为了链条中下一个的主机, 可以有效减轻主机的写压力,去中心化降低风险.
缺点: 一旦某个从机宕机,后面的从机都无法备份。
使6381从服务器成为6380的从服务器(6380为6379的从服务器)
还需要更详细的学习