Redis事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
Redis事务的主要作用就是串联多个命令防止别的命令插队。
从输入Multi命令开始,输入的命令都会依次进入命令队列,但不会执行,直到输入Exec命令后,redis才会将之前的命令队列中的命令依次执行
组队过程中,可以使用discard来放弃组队
事物的错误处理
举例:一个账户有A,B,C三个人同时使用,账户上一共有10000,A使用8000,B使用5000,C使用1000,如果没有锁的限制,那么账户上可能会出现-4000的情况
顾名思义,非常悲观,他认为所有的操作可能都会被别人打断,所以在所有的操作时都先加上一把锁,再开始执行命令,这时候如果别人想要访问这个数据时,是不允许对数据执行操作的,直到命令队列执行完毕才释放锁。悲观锁虽然可以解决例子中出现的问题,但是效率非常低。
顾名思义,非常乐观,,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种check-and-set机制实现事务的。
模拟数据
127.0.0.1:6379> set balance 100
OK
打开两个客户端,分别输入以下操作
# 第一个客户端
127.0.0.1:6379> watch balance # watch key 表示监视这个key
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> incrby balance 10
QUEUED
# 第二个客户端
127.0.0.1:6379> watch balance
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> incrby balance 20
QUEUED
分别执行
# 先执行第一个客户端,操作成功
127.0.0.1:6379(TX)> exec
1) (integer) 110
# 再执行第二个客户端,返回空,因为监视的key版本被修改,所以无法执行
127.0.0.1:6379(TX)> exec
(nil)
基本实现
//秒杀过程
public static boolean doSecKill(String uid,String prodid) throws IOException {
//1、判断uid、prodid不为空
if ("".equals(uid) || "".equals(prodid)){
return false;
}
//2、连接jedis
Jedis jedis = new Jedis("192.168.200.130",6379);
//3、拼接key:库存key、秒杀到的用户key
String kcKey = "sk:" + prodid + ":gt";
String userKey = "sk:" + prodid + ":user";
//4、获取库存:库存为null表示秒杀还没开始
String kc = jedis.get(kcKey);
if (kc == null){
System.out.println("秒杀还没有开始,请等待。。。");
jedis.close();
return false;
}
//5、判断用户是否以及秒杀成功,一个用户只允许购买一次
Set<String> users = jedis.smembers(userKey);
for (String user:users) {
if (uid.equals(user)){
System.out.println("您已秒杀成功,请把机会留给别人");
jedis.close();
return false;
}
}
//6、判断库存数量,小于等于0,秒杀结束
if (Integer.parseInt(kc) <= 0){
System.out.println("手慢啦!秒杀结束");
jedis.close();
return false;
}
//7、秒杀过程
//库存减一
jedis.decr(kcKey);
//加入名单
jedis.sadd(userKey,uid);
System.out.println("恭喜你,秒杀成功");
jedis.close();
return true;
}
使用ab测试
centos7需要自己安装ab工具
yum install httpd-tools
创建postfile文件,模拟表单提交参数,以&结尾,放在当前目录
vim postfile
测试
# -n 表示访问总数 -c 表示并发次数 -p 获取postfile文件 -T表单的enctype熟悉 访问地址
ab -n 2000 -c 200 -k -p ~/postfile -T application/x-www-form-urlencoded http://192.168.0.10:8080/doseckill
我们设置库存为10个,测试之后发现库存为-21,出现了超卖的问题
127.0.0.1:6379> get sk:0101:gt
"-21"
解决超时、超卖问题
解决超时:使用redis连接池
解决超卖:使用乐观锁
//秒杀过程
public static boolean doSecKill(String uid,String prodid) throws IOException {
//1、判断uid、prodid不为空
if ("".equals(uid) || "".equals(prodid)){
return false;
}
//2、连接jedis
// Jedis jedis = new Jedis("192.168.200.130",6379);
//使用redis连接池解决链接超时问题
JedisPool jedisPool = JedisPoolUtil.getJedisPoolInstance();
Jedis jedis = jedisPool.getResource();
//3、拼接key:库存key、秒杀到的用户key
String kcKey = "sk:" + prodid + ":gt";
String userKey = "sk:" + prodid + ":user";
//4、获取库存:库存为null表示秒杀还没开始
String kc = jedis.get(kcKey);
if (kc == null){
System.out.println("秒杀还没有开始,请等待。。。");
jedis.close();
return false;
}
//5、判断用户是否以及秒杀成功,一个用户只允许购买一次
Set<String> users = jedis.smembers(userKey);
for (String user:users) {
if (uid.equals(user)){
System.out.println("您已秒杀成功,请把机会留给别人");
jedis.close();
return false;
}
}
//6、判断库存数量,小于等于0,秒杀结束
if (Integer.parseInt(kc) <= 0){
System.out.println("手慢啦!秒杀结束");
jedis.close();
return false;
}
//7、秒杀过程:使用乐观锁解决超卖问题
jedis.watch(kcKey);
Transaction multi = jedis.multi();
// //库存减一
// jedis.decr(kcKey);
multi.decr(kcKey);
// //加入名单
// jedis.sadd(userKey,uid);
multi.sadd(userKey,uid);
List<Object> list = multi.exec();
//list返回值为空则代表执行失败
if (list == null || list.size() == 0){
System.out.println("秒杀失败");
jedis.close();
return false;
}
System.out.println("恭喜你,秒杀成功");
jedis.close();
return true;
}
库存遗留问题
给大家看一下为什么会出现库存遗留问题
使用LUA脚本可以解决这一问题,不要求掌握,只需要能看懂即可
local userid=KEYS[1];
local prodid=KEYS[2];
local qtkey="sk:"..prodid..":gt";
local usersKey="sk:"..prodid..":user';
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;
对Java代码进行如下修改
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..\":gt\";\r\n" +
"local usersKey='sk:'..prodid..\":user\";\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" ;
//秒杀过程
public static boolean doSecKill(String uid,String prodid) throws IOException {
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;
}
}