Redis 事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
Redis 事务的主要作用就是串联多个命令防止别的命令插队。
Multi、Exec、discard
Redis 事务中有 Multi、Exec 和 discard 三个指令,在 Redis 中,从输入 Multi 命令开始,输入的命令都会依次进入命令队列中,但不会执行,直到输入 Exec 后,Redis 会将之前的命令队列中的命令依次执行。而组队的过程中可以通过 discard 来放弃组队。
组队的错误处理
1. 编译错误
组队中某个命令出现了报告错误,执行时整个的所有队列都会被取消。
2. 运行错误
如果组队成功了,执行阶段某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚。
事务冲突问题
举个例子,三个人用同一个账号买东西,账号里面只有1w元,A买8k,B买5k,C买1k。
购买的流程先检查账号里面的钱有多少,在减去物品的钱。
如果没有锁,在同一个时间节点(很短的时间),三人同时购买,这时候都检测到账号里面有1w,够,都能买,然后8k,5k,1k的消费,三个人消费了14k,账号原本只有10k,现在还倒欠4k。
这是不允许的情况,这就是事务冲突。
悲观锁
悲观锁 (Pessimistic Lock),顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会 block 直到它拿到锁。
如上例子:
- A先拿到账号进行交易,数据1w,上锁,A购买成功,还剩2k,解锁 (A拿到账号期间,BC拿不到,阻塞状态)
- B先拿到账号进行交易,数据2k,上锁,B购买失败,还剩2k,解锁
- C先拿到账号进行交易,数据2k,上锁,C购买成功,还剩1k,解锁
传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
乐观锁
乐观锁 (Optimistic Lock),顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis 就是利用这种 check-and-set 机制实现事务的。
WATCH key [key …]
在执行 multi 之前,先执行 watch key1 [key2],可以监视一个 (或多个) key ,如果在事务执行exec之前,哪怕其中一个 key 被其他命令所改动,那么整个事务将被中断
unwatch
取消 WATCH 命令对所有 key 的监视。如果在执行 WATCH 命令之后,EXEC 命令或 DISCARD 命令先被执行了的话,那么就不需要再执行 UNWATCH 了。
简单秒杀案例
这个秒杀会有问题,比如超卖
package com.example.demo2.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
@Service
public class MsService {
@Autowired
@Qualifier("redisTemplate")
private RedisTemplate redisTemplate;
public boolean ms(String uid,String pid){
//1.判断用户id和商品id是否为空
if(uid==null||pid==null){
return false;
}
//2.拼接key
String ukey = "ms:"+pid+":u";//2.1秒杀成功的用户key
String pkey = "ms:"+pid+":p";//2.2商品的key
//3.获取库存,如果秒杀还没开始库存为空
Object pnum = redisTemplate.opsForValue().get(pkey);
if(pnum==null){
System.out.println("秒杀还没开始");
return false;
}
//4.判断用户是否重复秒杀
Boolean isuser = redisTemplate.opsForSet().isMember(ukey, uid);
if(isuser){
System.out.println("不能重复秒杀");
return false;
}
//5.判断库存数量,小于1,秒杀结束
if(Integer.parseInt(pnum.toString())<=0){
System.out.println("秒杀结束"+pnum);
return false;
}
//6.秒杀过程
redisTemplate.opsForValue().decrement(pkey);//秒杀数量-1
redisTemplate.opsForSet().add(ukey,uid);//把秒杀成功的用户加入到秒杀成功的set集合里面
System.out.println("秒杀成功"+pnum);
return true;
}
}
package com.example.demo2.controller;
import com.example.demo2.service.MsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Random;
@RestController
public class MsController {
@Autowired
private MsService msService;
@GetMapping("/ms")
public boolean ms(){
int uid = new Random().nextInt(10000);
String pid="1";
return msService.ms(String.valueOf(uid),pid);
}
}
改进版本秒杀
通过redis的事务,实现乐观锁。就能解决超卖问题了。
用JMeter测试,发现在同一时间,n个请求拿到下面代码里面的pkey,由于乐观锁的原因,这个n个请求中只有一个请求会成功,其他的全部会失败。这样就解决超卖问题,但是加油遗留问题,就是没卖完。遗留问题以后再讲
SpringBoot整合的Redis
在SpringBoot整合的Redis里面使用事务,用下面的方案,跟着直觉点的方法不行。
package com.example.demo2.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class MsService {
@Autowired
@Qualifier("redisTemplate")
private RedisTemplate redisTemplate;
public boolean ms(String uid,String pid){
//1.判断用户id和商品id是否为空
if(uid==null||pid==null){
return false;
}
//2.拼接key
String ukey = "ms:"+pid+":u";//2.1秒杀成功的用户key
String pkey = "ms:"+pid+":p";//2.2商品的key
//【加入事务】
List<Object> list = (List<Object>) redisTemplate.execute(new SessionCallback<List<Object>>() {
@Override
public List<Object> execute(RedisOperations operations) throws DataAccessException {
//【添加乐观锁】
operations.watch(pkey);
//3.获取库存,如果秒杀还没开始库存为空
Object pnum = operations.opsForValue().get(pkey);
if(pnum==null){
System.out.println("秒杀还没开始");
return null;
}
//4.判断用户是否重复秒杀
Boolean isuser = operations.opsForSet().isMember(ukey, uid);
if(isuser){
System.out.println("不能重复秒杀");
return null;
}
//5.判断库存数量,小于1,秒杀结束
if(Integer.parseInt(pnum.toString())<=0){
System.out.println("秒杀结束"+pnum);
return null;
}
operations.multi();
//6.秒杀过程
operations.opsForValue().decrement(pkey);//秒杀数量-1
operations.opsForSet().add(ukey, uid);//把秒杀成功的用户加入到秒杀成功的set集合里面
List list = operations.exec();
if(list==null||list.size()==0){
System.out.println("失败");
}else {
System.out.println("秒杀成功"+pnum);
}
return list;
}
});
return true;
}
}
直接用Jedis
package com.example.demo2.service;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Service;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;
import java.util.List;
@Service
public class JmsService {
public boolean jms(String uid,String pid){
Jedis jedis = new Jedis("xxx.xxx.xxx.xxx",6379);
jedis.auth("xxxxxx");
//1.判断用户id和商品id是否为空
if(uid==null||pid==null){
return false;
}
//2.拼接key
String ukey = "ms:"+pid+":u";//2.1秒杀成功的用户key
String pkey = "ms:"+pid+":p";//2.2商品的key
//【添加乐观锁】
jedis.watch(pkey);
//3.获取库存,如果秒杀还没开始库存为空
String pnum = jedis.get(pkey);
if(pnum==null){
System.out.println("秒杀还没开始");
jedis.close();
return false;
}
//4.判断用户是否重复秒杀
Boolean isuser = jedis.sismember(ukey, uid);
if(isuser){
System.out.println("不能重复秒杀");
jedis.close();
return false;
}
//5.判断库存数量,小于1,秒杀结束
if(Integer.parseInt(pnum)<=0){
System.out.println("秒杀结束"+pnum);
jedis.close();
return false;
}
Transaction transaction = jedis.multi();
//6.秒杀过程
transaction.decr(pkey);//秒杀数量-1
transaction.sadd(ukey, uid);//把秒杀成功的用户加入到秒杀成功的set集合里面
List list = transaction.exec();
if(list==null||list.size()==0){
System.out.println("失败");
jedis.close();
return false;
}else {
System.out.println("秒杀成功"+pnum);
}
jedis.close();
return true;
}
}