Redis的事务(简单的redis秒杀案例)

发布于 2021-12-21  2.08k 次阅读


Redis是单线程加多路IO复原的形式,MemCache是多线程加锁的显示

Redis本身是单线程的形式,所以Redis的操作是本身就具有原子性的,每执行的命令都是有原子性的

原子性:指不会被线程调度机制打断的操作

操作一旦执行,就一直运行到结束,中间不会有任何线程将操作切换进来

  • 在单线程中,能够再单条指令中完成的操作都可以被认为是"原子性操作",因为中断只能发生再指令之间
  • 在多线程中,不能被其他进程(线程)打断的操作也叫原则性操作

Redis的原则性主要由它本身的单线程所决定的

Redis也提供了事务的操作

一,Redis事务的概念

Redis事务是一个单独的隔离操作:事务中的所有命令都会被序列化,按顺序执行执行,事务在执行的过程中,不会被其他客户端发送来的命令请求所打断

Redis事务的主要作用:串联多个命令防止别的命令插队

Redis事务的三大命令:

  • Multi(组队阶段):开启一个事务,所有操作依次进入队列(不执行)
  • Exec(执行阶段):执行事务中的命令,依次执行
  • Discard:组队阶段中放弃组队

  • 组队阶段命令出错误,执行exec时这个事务的所有操作都会被取消
  • 执行阶段命令出错误,那个错误命令会报错不会执行,其他命令不受影响,这个时候它不像MySql是原子性的

一,Redis事务的冲突问题(悲观锁和乐观锁)

事务冲突主要发生在并发情况下,当多个用户同时修改一条数据时就会出现事务冲突问

例:现在有redis客户端,设置money为1000,有三个连接客户端分别请求服务器并修改money数据

解决这种事务冲突的方案主要有两种:

  • 悲观锁
  • 乐观锁

二,悲观锁

悲观锁,顾名思义就是对未来的操作很悲观,每一次拿数据的时候都认为别人会修改,所以在每一次在拿数据的时候都会上锁,这样别人想拿这个数据就需要先block锁,直到拿到锁,一次只有一个线程能拿到锁

悲观锁的实现本质上就是加锁,传统的关系型数据库就是使用悲观锁,比如MySql的行锁,表锁,读锁,写锁等都是悲观锁的实现

悲观锁本质上操作是串行的,没有获取到锁的对象需要进行等待,依次获取锁执行,效率很低

注:Redis并没有自带悲观锁的操作,要实现悲观锁需要使用外部编程的方式的实现

三,乐观锁

乐观锁,顾名思义就是很乐观,每一次拿数据的时候都认为其他用户不会做修改,所以不会上锁,但是在更新的时候会判断一下在此期间有没有用户去更新这个数据,它的实现主要提供版本号,可以版本号判断其他用户有没有做修改

乐观锁:用于多读的应用类型,这样可以提高吞吐量,Redis本身自带了乐观锁机制(Watch)

 

四,Redis乐观锁操作

Redis默认自带了乐观锁,通过命令 WATCH key [key……]

在执行Multi之前,先执行watch key1 [key2],可以监视一个或多个key,如果事务在exec执行之前监听的key被其他命令改动,那么事务就会被打断执行失败

命令:

  • watch key:监听key(本质上就是在key上面加一个版本号)
  • unwatch:取消监听

通过乐观锁和基本使用分析Redis的事务特性:

  • 单独的隔离操作:事务中的所有命令都会被序列化,按照顺序执行,事务在执行过程中不会被其他客户端发来的命令请求打断
  • 没有隔离级别的概念:队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行
  • 不保证原子性:事务中如果有一条命令执行失败,其后的命令依然会被执行,没有回滚

二,通过Redis实现简单秒杀案例

一,无事务秒杀

一,无事务秒杀代码

① controll层

@Controller
public class sckillcontrol {

    //秒杀的service
    @Autowired
    Seckillservice seckillservice;

    //虚拟用户id,没有前端不能从前端获取
    Random random = new Random();

    @RequestMapping("/seckill")
    @ResponseBody
    public String seckill(){

        //没有前端,固定商品id
        String commodity_id="1001";

        //每一次秒杀随机用户id
        int userid = random.nextInt(1000);

        //传入service
        boolean b = seckillservice.seckill_service(commodity_id, userid);
        if(!b){
            return "秒杀失败!";
        }
        return "秒杀成功!";
    }

② service层

@Service
public class Seckillservice {

    @Autowired
    RedisTemplate redisTemplate;

    public boolean seckill_service(String commodityid,int userid){

        //1,判断商品id和用户id是否为空
        if(commodityid.equals("") || userid==0 ){
            return false;
        }

        //2,对commodityid和userid做为key进行拼接
        String commodity = "commodit:"+commodityid;
        String user = "user:size";

        //3,判断redis中commodityid是否存在,不存在则秒杀没有开始
        Object o = redisTemplate.opsForValue().get(commodity);
        if(o==null){
            System.out.println("秒杀未开始!");
            return false;
        }


        //4,判断commodity是否还要库存
        Integer quantity  = (Integer)redisTemplate.opsForValue().get(commodity);
        if(quantity>1){
            System.out.println("库存不够!");
            return false;
        }


        //5,判断用户是否重复操作
        if(redisTemplate.opsForSet().isMember(user,userid)){
            System.out.println("用户重复操作!");
            return false;
        }


        //6,秒杀库存
        //库存减一
        redisTemplate.opsForValue().decrement(commodity, 1);
        //秒杀成功的用户添加到用户set集合中
        redisTemplate.opsForSet().add(user,userid);
        return true;
    }
}
③ 单线程测试
将库存设置为10

运行10次之后