https://gitee.com/DanShenGuiZu/learnDemo/tree/master/redis-learn/jedis
串联多个命令防止别的命令插队。
从输入Multi命令开始,输入的命令都会依次进入命令队列中,但不会执行,直到输入Exec后,Redis会将之前的命令队列中的命令依次执行。
组队的过程中可以通过discard来放弃组队。
127.0.0.1:6379> Multi
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) OK
127.0.0.1:6379>
结果:所有命令都成功执行
127.0.0.1:6379> Multi
OK
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> set k4
(error) ERR wrong number of arguments for 'set' command
127.0.0.1:6379(TX)> exec
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379>
结果:所有命令都不执行
127.0.0.1:6379> Multi
OK
127.0.0.1:6379(TX)> set k5 v5
QUEUED
127.0.0.1:6379(TX)> incr k5
QUEUED
127.0.0.1:6379(TX)> set k6 v6
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) (error) ERR value is not an integer or out of range
3) OK
127.0.0.1:6379>
结果:除了incr k5 ,其他命令都成功
127.0.0.1:6379> Multi
OK
127.0.0.1:6379(TX)> set k5 v5
QUEUED
127.0.0.1:6379(TX)> incr k5
QUEUED
127.0.0.1:6379(TX)> set k6 v6
QUEUED
127.0.0.1:6379(TX)> discard
OK
127.0.0.1:6379>
想想一个场景:有很多人有你的账户,同时去参加双十一抢购,会引起事务冲突问题,
一个请求想给金额减8000
一个请求想给金额减5000
一个请求想给金额减1000
悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会堵塞,直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。
Redis就是利用乐观锁check-and-set机制实现事务的。
# 当前票数为1
127.0.0.1:6379> set ticket 1
OK
# 用户A的钱500
127.0.0.1:6379> set money 500
OK
# 设置锁,如果票数发生变化,下面的事务不执行
127.0.0.1:6379> watch ticket
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> decr ticket
QUEUED
127.0.0.1:6379> decrby money 100
QUEUED
# 在exec之前,另一个用户B购买了票数,之后当前用户A执行exec
127.0.0.1:6379> exec
(nil) :表示用户A的事务没有生效
# 事务里面的命令都没有生效,所以钱还是500
127.0.0.1:6379> get money
"500"
另一个用户B,在用户A exec之前,先买了票
127.0.0.1:6379> decr ticket
(integer) 0
127.0.0.1:6379>
取消 WATCH 命令对所有 key 的监视。
如果在执行 WATCH 命令之后,EXEC 命令或DISCARD 命令先被执行了的话,那么就不需要再执行UNWATCH 了,因为WATCH监控只会持续到exec命令。
https://blog.csdn.net/zhou920786312/article/details/122769472
127.0.0.1:6379> flushdb
OK
设置商品111的库存为100
127.0.0.1:6379> set stock:111 100
127.0.0.1:6379>
/**
* 秒杀案例
*
* @author 周飞
* @class: RedisTestController
* @date 2022/2/3 14:11
* @Verson 1.0 -2022/2/3 14:11
* @see
*/
public class DoSecKill_v1 {
/**
*
* @param userId 用户id
* @param prodId 商品id
* @return boolean
* @author 周飞
* @since 2022/2/3 16:01
*/
// 秒杀过程
public static boolean doSecKill(String userId, String prodId) throws IOException {
// 库存 key
String stock_key = "stock:" + prodId;
// 秒杀成功用户key
String success_user_key = "success:" + prodId;
// 获取jedis
Jedis jedis = getJedis_v1();
// 监视库存
jedis.watch(stock_key);
// 判断用户是否重复秒杀操作
if (jedis.sismember(success_user_key, userId)) {
System.out.println("该用户已经秒杀成功");
jedis.close();
return false;
}
// 获取库存数量
String stock_count = jedis.get(stock_key);
// 判断如果商品数量,库存数量小于1,秒杀结束
if (Integer.parseInt(stock_count) <= 0) {
System.out.println("秒杀已经结束了");
jedis.close();
return false;
}
// 库存-1
jedis.decr(stock_key);
// 把秒杀成功用户添加清单里面
jedis.sadd(success_user_key, userId);
System.out.println("秒杀成功了..");
jedis.close();
return true;
}
/**
* 构建Jedis
*
* @author 周飞
* @since 2022/2/3 15:18
*/
public static Jedis getJedis_v1() {
// 连接redis
return new Jedis("192.168.187.138", 6379);
}
}
@RestController
public class SecondKillController {
/**
* @param prodId 商品id
* @author 周飞
* @since 2022/2/3 14:51
*/
@RequestMapping("/second_kill")
public boolean secondKill(@RequestParam("prodId") String prodId) throws IOException {
String userId = new Random().nextInt(50000) + "";
return DoSecKill_v1.doSecKill(userId, prodId);
}
}
ab -n 2000 -c 200 -k http://192.168.43.45:8080/second_kill?prodId=111
// 利用乐观锁淘汰用户,解决超卖问题
// 使用事务
Transaction multi = jedis.multi();
// 组队操作
multi.decr(stock_key);
multi.sadd(success_user_key, userId);
// 执行
List
127.0.0.1:6379> flushdb
OK
设置商品111的库存为100
127.0.0.1:6379> set stock:111 100
127.0.0.1:6379>
ab -n 2000 -c 200 -k http://192.168.43.45:8080/second_kill?prodId=111
通过lua脚本解决超卖问题,实际上是redis 利用其单线程的特性,用任务队列的方式解决多任务并发问题。
local userId=KEYS[1];
local prodId=KEYS[2];
local stock_key='stock:'..prodId;
local success_user_key='success:'..prodId;
local userExists=redis.call('sismember',success_user_key,userId);
if tonumber(userExists)==1 then
return 2;
end
local num= redis.call('get' ,stock_key);
if tonumber(num)<=0 then
return 0;
else
redis.call('decr',stock_key);
redis.call('sadd',success_user_key,userId);
end
return 1
/**
* 秒杀案例
*
* @author 周飞
* @class: RedisTestController
* @date 2022/2/3 14:11
* @Verson 1.0 -2022/2/3 14:11
* @see
*/
public class DoSecKill_v3 {
public static void main(String[] args) throws IOException {
doSecKill("201","111");
}
static String secKillScript =
"local userId=KEYS[1]; \n" +
"local prodId=KEYS[2];\n" +
"local stock_key=\"stock:\"..prodId;\n" +
"local success_user_key=\"success:\"..prodId; \n" +
"local userExists=redis.call(\"sismember\",success_user_key,userId);\n" +
"if tonumber(userExists)==1 then \n" +
" return 2;\n" +
"end\n" +
"local num= redis.call(\"get\" ,stock_key);\n" +
"if tonumber(num)<=0 then \n" +
" return 0; \n" +
"else \n" +
" redis.call(\"decr\",stock_key);\n" +
" redis.call(\"sadd\",success_user_key,userId);\n" +
"end\n" +
"return 1 " ;
public static boolean doSecKill(String uid,String prodid) throws IOException {
Jedis jedis=getJedis_v1();
//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;
}
/**
* 构建Jedis
*
* @author 周飞
* @since 2022/2/3 15:18
*/
public static Jedis getJedis_v1() {
// 2 连接redis
return new Jedis("192.168.187.138", 6379);
}
}
127.0.0.1:6379> flushdb
OK
设置商品111的库存为100
127.0.0.1:6379> set stock:111 100
127.0.0.1:6379>
ab -n 2000 -c 200 -k http://192.168.43.45:8080/second_kill?prodId=111
lua 脚本代码 按照任务队列依次执行,本质就是redis单线程执行任务。