事务的定义

Redis事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断
Redis事务的主要作用就是串联多个命令防止别的命令插队

Multi、Exec、discard

从输入Multi命令开始,输入的命令都会依次进入命令队列中(Multi),但不会执行,直到输入Exec后,Redis会将之前的命令队列中的命令依次执行。exec后事务执行完成。组队的过程中可以通过discard来放弃组队
image.pngimage.png

事务的错误处理

组队中某个命令出现了报告错误,执行时整个的所有队列都会被取消
如果执行阶段某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚

组队过程中错误

image.png

执行过程中错误

image.png

事务的冲突问题

悲观锁(Pessimistic Lock)

每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁表锁等,读锁写锁等,都是在做操作之前先上锁

乐观锁(Optimistic Lock)

每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量Redis就是利用这种check-and-set机制实现事务

乐观锁演示

  1. //监视一个或者多个key
  2. WATCH key [key...]
  3. //取消 WATCH 命令对所有 key 的监视。
  4. unwatch
  5. //如果在执行 WATCH 命令之后,EXEC 命令或DISCARD 命令先被执行了的话,那么就不需要再执行UNWATCH 了。

在执行multi之前,先执行watch key1 [key2],监视一个(或多个) key 如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断

先执行
image.png
后执行
所监视的key被改变,版本不一致,事务执行打断
image.png

Redis事务三特性

单独的隔离操作

事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断

没有隔离级别的概念

队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行

不保证原子性

事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚

案例——秒杀

  • 使用事务和监视解决超卖问题

    1. @RestController
    2. public class TestSecKill {
    3. //重写了bean就要使用相对应的类型 不然 就会用成原生的bean
    4. @Resource
    5. private RedisTemplate<String,Object> redisTemplate;
    6. @GetMapping("/seckill")
    7. public String secKill() {
    8. String uid = UUID.randomUUID().toString().substring(1, 10);
    9. Boolean isSucc = doSecKill(uid);
    10. if (isSucc) {
    11. System.out.println("秒杀成功");
    12. return "秒杀成功";
    13. } else {
    14. System.out.println("==秒杀失败==");
    15. return "==秒杀失败==";
    16. }
    17. }
    18. private Boolean doSecKill(String uid) {
    19. //判空处理
    20. if (uid == null) {
    21. return false;
    22. }
    23. String skUser = "sk:user:"+uid;
    24. String skQt = "sk:1001:qt";
    25. //开启监视
    26. redisTemplate.watch(skQt);
    27. //库存判断
    28. //1 库存为空(未开始秒杀) 2 库存为0(秒杀结束)
    29. String redisQt = String.valueOf(redisTemplate.opsForValue().get(skQt));
    30. if (redisQt == null || Integer.parseInt(redisQt) <= 0) {
    31. System.out.println("========秒杀结束======");
    32. return false;
    33. }
    34. //是否重复抢购
    35. Set<Object> members = redisTemplate.opsForSet().members(skUser);
    36. if (members.size() != 0) {
    37. return false;
    38. }
    39. //记录库存、秒杀成功人员
    40. //开启事务防止多线程情况下 其它请求影响当前redis命令
    41. redisTemplate.setEnableTransactionSupport(true);
    42. try {
    43. redisTemplate.multi();
    44. redisTemplate.opsForValue().decrement(skQt);
    45. redisTemplate.opsForSet().add(skUser, uid);
    46. List<Object> execList = redisTemplate.exec();
    47. if (execList.size() == 0) {
    48. return false;
    49. }
    50. } catch (Exception e) {
    51. //开启回滚
    52. redisTemplate.discard();
    53. }
    54. return true;
    55. }
    56. }
    1. #linux下用 ab 对单接口压测,模拟多并发 (简单的DDOS攻击)
    2. ab -i -n 2000 -c 200 http://192.168.31.228:9090/seckill
  • 库存遗留问题:因为乐观锁操作数据时会修改数据的版本号,就会造成数据虽然存在但是版本号不一致不能购买。

通过lua脚本解决争抢问题,实际上是redis 利用其单线程的特性,用任务队列的方式解决多任务并发问题。一次提交给redis执行,减少反复连接redis的次数。提升性能。