3.redis-事务
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}")@ResponseBodypublic 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);} }