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

Multi、Exec、discard

Redis 事务中有 Multi、Exec 和 discard 三个指令,在 Redis 中,从输入 Multi 命令开始,输入的命令都会依次进入命令队列中,但不会执行,直到输入 Exec 后,Redis 会将之前的命令队列中的命令依次执行。而组队的过程中可以通过 discard 来放弃组队。
image.png
image.png

组队的错误处理

1. 编译错误

组队中某个命令出现了报告错误,执行时整个的所有队列都会被取消。
image.png
image.png

2. 运行错误

如果组队成功了,执行阶段某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚。
image.png
image.png

事务冲突问题

举个例子,三个人用同一个账号买东西,账号里面只有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 被其他命令所改动,那么整个事务将被中断

如下例子
image.pngimage.png

unwatch

取消 WATCH 命令对所有 key 的监视。如果在执行 WATCH 命令之后,EXEC 命令或 DISCARD 命令先被执行了的话,那么就不需要再执行 UNWATCH 了。

简单秒杀案例

这个秒杀会有问题,比如超卖

  1. package com.example.demo2.service;
  2. import org.springframework.beans.factory.annotation.Autowired;
  3. import org.springframework.beans.factory.annotation.Qualifier;
  4. import org.springframework.data.redis.core.RedisTemplate;
  5. import org.springframework.stereotype.Service;
  6. @Service
  7. public class MsService {
  8. @Autowired
  9. @Qualifier("redisTemplate")
  10. private RedisTemplate redisTemplate;
  11. public boolean ms(String uid,String pid){
  12. //1.判断用户id和商品id是否为空
  13. if(uid==null||pid==null){
  14. return false;
  15. }
  16. //2.拼接key
  17. String ukey = "ms:"+pid+":u";//2.1秒杀成功的用户key
  18. String pkey = "ms:"+pid+":p";//2.2商品的key
  19. //3.获取库存,如果秒杀还没开始库存为空
  20. Object pnum = redisTemplate.opsForValue().get(pkey);
  21. if(pnum==null){
  22. System.out.println("秒杀还没开始");
  23. return false;
  24. }
  25. //4.判断用户是否重复秒杀
  26. Boolean isuser = redisTemplate.opsForSet().isMember(ukey, uid);
  27. if(isuser){
  28. System.out.println("不能重复秒杀");
  29. return false;
  30. }
  31. //5.判断库存数量,小于1,秒杀结束
  32. if(Integer.parseInt(pnum.toString())<=0){
  33. System.out.println("秒杀结束"+pnum);
  34. return false;
  35. }
  36. //6.秒杀过程
  37. redisTemplate.opsForValue().decrement(pkey);//秒杀数量-1
  38. redisTemplate.opsForSet().add(ukey,uid);//把秒杀成功的用户加入到秒杀成功的set集合里面
  39. System.out.println("秒杀成功"+pnum);
  40. return true;
  41. }
  42. }
  1. package com.example.demo2.controller;
  2. import com.example.demo2.service.MsService;
  3. import org.springframework.beans.factory.annotation.Autowired;
  4. import org.springframework.web.bind.annotation.GetMapping;
  5. import org.springframework.web.bind.annotation.RestController;
  6. import java.util.Random;
  7. @RestController
  8. public class MsController {
  9. @Autowired
  10. private MsService msService;
  11. @GetMapping("/ms")
  12. public boolean ms(){
  13. int uid = new Random().nextInt(10000);
  14. String pid="1";
  15. return msService.ms(String.valueOf(uid),pid);
  16. }
  17. }

改进版本秒杀

通过redis的事务,实现乐观锁。就能解决超卖问题了。
用JMeter测试,发现在同一时间,n个请求拿到下面代码里面的pkey,由于乐观锁的原因,这个n个请求中只有一个请求会成功,其他的全部会失败。这样就解决超卖问题,但是加油遗留问题,就是没卖完。遗留问题以后再讲

SpringBoot整合的Redis

在SpringBoot整合的Redis里面使用事务,用下面的方案,跟着直觉点的方法不行。

  1. package com.example.demo2.service;
  2. import org.springframework.beans.factory.annotation.Autowired;
  3. import org.springframework.beans.factory.annotation.Qualifier;
  4. import org.springframework.dao.DataAccessException;
  5. import org.springframework.data.redis.core.RedisOperations;
  6. import org.springframework.data.redis.core.RedisTemplate;
  7. import org.springframework.data.redis.core.SessionCallback;
  8. import org.springframework.stereotype.Service;
  9. import java.util.List;
  10. @Service
  11. public class MsService {
  12. @Autowired
  13. @Qualifier("redisTemplate")
  14. private RedisTemplate redisTemplate;
  15. public boolean ms(String uid,String pid){
  16. //1.判断用户id和商品id是否为空
  17. if(uid==null||pid==null){
  18. return false;
  19. }
  20. //2.拼接key
  21. String ukey = "ms:"+pid+":u";//2.1秒杀成功的用户key
  22. String pkey = "ms:"+pid+":p";//2.2商品的key
  23. //【加入事务】
  24. List<Object> list = (List<Object>) redisTemplate.execute(new SessionCallback<List<Object>>() {
  25. @Override
  26. public List<Object> execute(RedisOperations operations) throws DataAccessException {
  27. //【添加乐观锁】
  28. operations.watch(pkey);
  29. //3.获取库存,如果秒杀还没开始库存为空
  30. Object pnum = operations.opsForValue().get(pkey);
  31. if(pnum==null){
  32. System.out.println("秒杀还没开始");
  33. return null;
  34. }
  35. //4.判断用户是否重复秒杀
  36. Boolean isuser = operations.opsForSet().isMember(ukey, uid);
  37. if(isuser){
  38. System.out.println("不能重复秒杀");
  39. return null;
  40. }
  41. //5.判断库存数量,小于1,秒杀结束
  42. if(Integer.parseInt(pnum.toString())<=0){
  43. System.out.println("秒杀结束"+pnum);
  44. return null;
  45. }
  46. operations.multi();
  47. //6.秒杀过程
  48. operations.opsForValue().decrement(pkey);//秒杀数量-1
  49. operations.opsForSet().add(ukey, uid);//把秒杀成功的用户加入到秒杀成功的set集合里面
  50. List list = operations.exec();
  51. if(list==null||list.size()==0){
  52. System.out.println("失败");
  53. }else {
  54. System.out.println("秒杀成功"+pnum);
  55. }
  56. return list;
  57. }
  58. });
  59. return true;
  60. }
  61. }

直接用Jedis

  1. package com.example.demo2.service;
  2. import org.springframework.context.annotation.Bean;
  3. import org.springframework.stereotype.Service;
  4. import redis.clients.jedis.Jedis;
  5. import redis.clients.jedis.Transaction;
  6. import java.util.List;
  7. @Service
  8. public class JmsService {
  9. public boolean jms(String uid,String pid){
  10. Jedis jedis = new Jedis("xxx.xxx.xxx.xxx",6379);
  11. jedis.auth("xxxxxx");
  12. //1.判断用户id和商品id是否为空
  13. if(uid==null||pid==null){
  14. return false;
  15. }
  16. //2.拼接key
  17. String ukey = "ms:"+pid+":u";//2.1秒杀成功的用户key
  18. String pkey = "ms:"+pid+":p";//2.2商品的key
  19. //【添加乐观锁】
  20. jedis.watch(pkey);
  21. //3.获取库存,如果秒杀还没开始库存为空
  22. String pnum = jedis.get(pkey);
  23. if(pnum==null){
  24. System.out.println("秒杀还没开始");
  25. jedis.close();
  26. return false;
  27. }
  28. //4.判断用户是否重复秒杀
  29. Boolean isuser = jedis.sismember(ukey, uid);
  30. if(isuser){
  31. System.out.println("不能重复秒杀");
  32. jedis.close();
  33. return false;
  34. }
  35. //5.判断库存数量,小于1,秒杀结束
  36. if(Integer.parseInt(pnum)<=0){
  37. System.out.println("秒杀结束"+pnum);
  38. jedis.close();
  39. return false;
  40. }
  41. Transaction transaction = jedis.multi();
  42. //6.秒杀过程
  43. transaction.decr(pkey);//秒杀数量-1
  44. transaction.sadd(ukey, uid);//把秒杀成功的用户加入到秒杀成功的set集合里面
  45. List list = transaction.exec();
  46. if(list==null||list.size()==0){
  47. System.out.println("失败");
  48. jedis.close();
  49. return false;
  50. }else {
  51. System.out.println("秒杀成功"+pnum);
  52. }
  53. jedis.close();
  54. return true;
  55. }
  56. }