01-Redis事务概述
- 概述
- Redis事务是一个单独的隔离操作:事务中的所有命令都会序列化放到一个队列中按顺序地执行。事务 在执行的过程中,不会被其他客户端发送来的命令请求所打断。
- 不支持ACID
- ①atomicity, 原子性, redis事务中的指令执行失败, 不影响后续指令执行;
- ②durability, 持久性, redis数据存储在内存中, 可以使用AOF和RDB进行补充.
02-Redis事务指令
-
语法
#开启事务, 后续指令放入到事务队列中 multi #提交事务, 执行事务队列中的指令, 结束事务 exec #回滚事务, 清空事务队列中的指令,结束事务 discard
-
代码实现1: 成功
-
注意事项
exec指令
执行之后可能会有多个结果, 要使用集合
来封装.
03-Redis事务错误处理
-
①编译期错误: 语法错误, 后续指令不能执行
-
②运行时错误: 语法正确, 执行错误. 不影响后续指令执行.
04-Redis锁机制
-
事务冲突问题
-
张三账户余额有10000元,他想在双11的时候抢购三款手机:mate20pro(价值1000元)、 mate30pro(价值5000元)、mate40pro(价值8000元),为了增加抢购成功率,他把账户给了另外两个 好朋友:李四、王五,让他们在双11帮忙抢购。结果,双11,他们三个人同时分别抢到了 mate20pro、mate30pro、mate40pro,然后他们都同时付了款…
-
悲观锁
-
效率低
-
乐观锁
-
总结
- Redis采用CAS乐观锁.
05-Redis乐观锁
- 代码实现
06-秒杀案例需求分析及准备
-
需求
- 秒杀同一件商品(一件商品多个库存)
-
分析
-
①商品库存
- string类型, key=seckill:商品id:kc
-
②秒杀成功者清单
- set类型, key=seckill:商品id:usr
-
准备
set seckill:0101:kc 100
07-秒杀案例基本功能
-
开发步骤
- ①编写前端代码
- ②编写表现层代码
- ③编写SeckillUtils工具类
- 执行秒杀
-
①编写前端代码
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>首页</title> <script src="script/vue.js"></script> <script src="script/axios.js"></script> </head> <body> <div id="app"> <button @click="handleSeckill">开始秒杀</button> </div> <script> Object.assign(window, Vue); const App = { setup() { const data = reactive({ cid: "1001" }) const handleSeckill = () => { axios({ method:"post", url:"/doSeckill", params:{ cid:data.cid } }).then((res) => { const {msg}=res.data; alert(msg); console.log(res.data); }) } return { data, handleSeckill } } } Vue.createApp(App).mount('#app'); </script> </body> </html>
-
②编写表现层代码
@Controller public class SeckillController { @PostMapping("/doSeckill/{cid}") @ResponseBody public ResultVO<Object> doSeckill(@PathVariable String cid){ //执行秒杀 String uid = UUID.randomUUID().toString().replace("-", ""); ResultVO<Object> result= SeckillUtils.doSeckill(uid,cid); System.out.println("result = " + result); return result; } }
-
编写jedis.properties配置问价
# redis主机地址 jedis.host=192.168.199.110 # redis端口 jedis.port=6379 # 最大连接数 jedis.maxTotal=1000 # 控制一个pool最多有多少个状态为空闲的jedis实例 jedis.maxIdle=100 # 最大的等待毫秒数,如果超过等待时间,则直接抛JedisConnectionException jedis.maxWaitMillis=10000
-
③编写ResultVo.java工具类
@NoArgsConstructor @AllArgsConstructor @Data public class ResultVO<T> { private boolean flag; private String msg; private T data; }
-
④编写JedisUtils工具类
public class JedisUtils { private static JedisPool jedisPool; static { InputStream inputStream = JedisUtils.class.getClassLoader().getResourceAsStream("jedis.properties"); Properties properties = new Properties(); try { properties.load(inputStream); int maxTotal = Integer.valueOf(properties.getProperty("jedis.maxTotal")); int maxIdle = Integer.valueOf(properties.getProperty("jedis.maxIdle")); long maxWaitMillis = Integer.valueOf(properties.getProperty("jedis.maxWaitMillis")); String host = properties.getProperty("jedis.host"); int port = Integer.valueOf(properties.getProperty("jedis.port")); //创建Jedis链接池对象 JedisPoolConfig poolConfig = new JedisPoolConfig(); poolConfig.setMaxTotal(maxTotal); poolConfig.setMaxIdle(maxIdle); poolConfig.setMaxWaitMillis(maxWaitMillis); jedisPool = new JedisPool( poolConfig, host, port ); } catch (IOException e) { throw new RuntimeException(e); } } public static Jedis getJedis() { return jedisPool.getResource(); } public static void close(Jedis jedis) { if ( null != jedis) { jedis.close(); jedis = null; } } }
-
③编写SeckillUtils工具类
public class SeckillUtils { /** * 执行秒杀 * @param uid 用户id * @param cid 商品id * @return */ public static ResultVO<Object> doSeckill(String uid, String cid) { //1.生成库存key [string:存储简单类型] [set seckill:1001:kc 100] String kcKey="seckill:"+cid+":kc"; System.out.println("kcKey = " + kcKey); //2.生成秒杀成功者清单key [set:存储多个结果且去重] [sadd seckill:1001:user user1 user2...] String userKey="seckill:"+cid+":user"; //3.判断秒杀是否开始 Jedis jedis = JedisUtils.getJedis(); String kcStr = jedis.get(kcKey); System.out.println("kcStr = " + kcStr); if(kcStr==null){ JedisUtils.close(jedis); return new ResultVO<Object>( false, "秒杀未开始,敬请期待!", null ); } //4.判断秒杀是否结束 int kc = Integer.parseInt(kcStr); if(kc<=0){ JedisUtils.close(jedis); return new ResultVO<Object>( false, "秒杀已结束!", null ); } //5.判断当前用户是否参加过秒杀 if (jedis.sismember(userKey,uid)) { JedisUtils.close(jedis); return new ResultVO<Object>( false, "不能重复参加秒杀活动!", null ); } //6.开始秒杀 //库存减一 jedis.decr(kcKey); //记录秒杀成功的用户 jedis.sadd(userKey,uid); JedisUtils.close(jedis); return new ResultVO<Object>( false, "秒杀成功!", null ); } }
08-ab工具模拟秒杀高并发
-
超卖问题
-
有多个用户同时执行了秒杀操作,且都认为当前有库存,都执行了库存减一,所以就出现了超卖问 题。
-
使用ab插件模拟高并发.
-
开发步骤
- ①安装ab插件
- yum -y install httpd-tools
- ②创建请求参数文件
/usr/local/data.txt
- ③开始模拟高并发
- ①安装ab插件
-
①安装ab插件
-
②创建请求参数文件
cid=1001&
-
③开始模拟高并发
ab -n 1000 -c 200 -p /usr/local/data.txt -T "application/x-www-form-urlencoded" 192.168.13.54:8080/doSeckill
09-秒杀案例解决超卖问题
-
分析
- 利用乐观锁淘汰用户,解决超卖问题。
-
代码实现
/** * 解决超卖问题 */ public class SeckillUtils1 { /** * 执行秒杀 * @param uid 用户id * @param cid 商品id * @return */ public static ResultVO<Object> doSeckill(String uid, String cid) { //1.生成库存key [string:存储简单类型] [set seckill:1001:kc 100] String kcKey="seckill:"+cid+":kc"; System.out.println("kcKey = " + kcKey); //2.生成秒杀成功者清单key [set:存储多个结果且去重] [sadd seckill:1001:user user1 user2...] String userKey="seckill:"+cid+":user"; //3.判断秒杀是否开始 Jedis jedis = JedisUtils.getJedis(); //4.给库存key加上乐观锁 jedis.watch(kcKey); String kcStr = jedis.get(kcKey); System.out.println("kcStr = " + kcStr); if(kcStr==null){ JedisUtils.close(jedis); return new ResultVO<Object>( false, "秒杀未开始,敬请期待!", null ); } //4.判断秒杀是否结束 int kc = Integer.parseInt(kcStr); if(kc<=0){ JedisUtils.close(jedis); return new ResultVO<Object>( false, "秒杀已结束!", null ); } //5.判断当前用户是否参加过秒杀 if (jedis.sismember(userKey,uid)) { JedisUtils.close(jedis); return new ResultVO<Object>( false, "不能重复参加秒杀活动!", null ); } //6.开始秒杀 //开启事务 Transaction multi = jedis.multi(); //库存减一 multi.decr(kcKey); //记录秒杀成功的用户 multi.sadd(userKey,uid); //提交事务 List<Object> result = multi.exec(); //判断事务是否执行失败 if ( result==null || result.size()==0) { JedisUtils.close(jedis); return new ResultVO<Object>( false, "商品已被抢走!", null ); } JedisUtils.close(jedis); return new ResultVO<Object>( false, "秒杀成功!", null ); } }
10-秒杀案例解决库存遗留问题
-
概述
- “ab -n 1000”,ab模拟高并发,意味着有1000个用户在参与秒杀,如果库存1000的话,商品绝对是 可以秒杀完的;
- “ab -n 1000 -c 200”,重新解读下ab模拟高并发,1000次请求,最高会有200个请求是并发的,意味 着会有200个请求抢同一件商品,如果是这种情况,就会存在明明有1000个人抢,但是只被抢走5个商 品,就会出现库存遗留问题。
-
概述
- Lua是一个小巧的脚本语言,Lua脚本可以很容易的被C/C++ 代码调用,也可以反过来调用C/C++的 函数,Lua并没有提供强大的库,一个完整的Lua解释器不过200k,所以Lua不适合作为开发独立应用 程序的语言,而是作为嵌入式脚本语言。
- 将复杂的或者多步的redis操作,写为一个脚本,一次提交给redis执行,减少反复连接redis的次数。 提升性能。
- LUA脚本是类似redis事务,有一定的原子性,不会被其他命令插队,可以替代redis事务完成一些的操 作。
-
开发步骤
- ①修改SeckillUtils工具类
- 引入LUA脚本
- ②代码测试
- ①修改SeckillUtils工具类
-
①修改SeckillUtils工具类
/** * LUA脚本解决库存遗留问题 */ public class SeckillUtils { //将之前的多次请求,使用LUA脚本拼接成一次请求 static String secKillScript = "local uid=KEYS[1];\r\n" + "local cid=KEYS[2];\r\n" + "local qtkey='seckill:'..cid..\":kc\";\r\n" + "local usersKey='seckill:'..cid..\":usr\";\r\n" + "local userExists=redis.call(\"sismember\",usersKey,uid);\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,uid);\r\n" + "end\r\n" + "return 1"; public static ResultVO<Object> doSeckill(String uid, String prodid) throws IOException { Jedis jedis = JedisUtils.getJedis(); String sha1 = jedis.scriptLoad(secKillScript); Object result = jedis.evalsha(sha1, 2, uid, prodid); String reString = String.valueOf(result); if ("0".equals(reString)) { System.out.println("失败!秒杀已经结束!"); JedisUtils.close(jedis); return new ResultVO<Object>(false, "秒杀已经结束!", null); } else if ("1".equals(reString)) { System.out.println("抢购成功!!!!"); } else if ("2".equals(reString)) { System.out.println("失败!您已经秒杀成功过了!把机会给别人吧!"); JedisUtils.close(jedis); return new ResultVO<Object>(false, "不能重复秒杀!", null); } else { System.err.println("抢购异常!!"); JedisUtils.close(jedis); return new ResultVO<Object>(false, "抢购异常!", null); } JedisUtils.close(jedis); return new ResultVO<Object>(true, "秒杀成功!", null); } }