springBoot整合redis以及事务的处理

Jedis

Jedis是一个java的模块,用来连接和操作我们的Redis数据库。类似于以前我们用我们的jdbc操作我们的mysql数据库。

在使用Jedis 连接redis之前,需要修改redis.conf的配置文件:

  1. redis.conf中注释掉bind 127.0.0.1(127.0.0.1表示只支持localhost连接,不支持远程连接)
  2. protected-mode no(关闭protected-mode模式,此时外部网络可以直接访问;开启protected-mode保护模式,需配置bind ip或者设置访问密码)
  3. systemctl status firewalld关闭防火墙 

 spring整合redis

1、\pom文件中引入 jar包:

        
            redis.clients
            jedis
            3.2.0
        

2、连接测试

    public static void main(String[] args) {
        //创建Jedis对象
        Jedis jedis = new Jedis("192.168.***.***",6379);
        //测试
        String value = jedis.ping();
        System.out.println(value);
    }

3、jedis对各中数据类型的操作

  //操作zset
    @Test
    public void demo5(){
        //创建Jedis对象
        Jedis jedis = new Jedis("192.168.***.***",6379);

        jedis.zadd("china",100d,"上海");
        jedis.zadd("china",200d,"北京");

        System.out.println(jedis.zrange("china",0,-1));

        Set keys = jedis.keys("*");
        for (String str: keys) {
            System.out.println(str);
        }
    }

    //操作hash
    @Test
    public void demo4(){
        //创建Jedis对象
        Jedis jedis = new Jedis("192.168.***.***",6379);

        jedis.hset("users", "age", "20");
        String hget = jedis.hget("users", "age");
        System.out.println(hget);

        Set keys = jedis.keys("*");
        for (String str: keys) {
            System.out.println(str);
        }
    }

    //操作set
    @Test
    public void demo3(){
        //创建Jedis对象
        Jedis jedis = new Jedis("192.168.***.***",6379);

        jedis.sadd("names","lucy","jack");
        Set names = jedis.smembers("names");
        System.out.println(names);

        jedis.srem("names","lucy");
        System.out.println(jedis.smembers("names"));

        Set keys = jedis.keys("*");
        for (String str: keys) {
            System.out.println(str);
        }
    }

    //操作List
    @Test
    public void demo2(){
        //创建Jedis对象
        Jedis jedis = new Jedis("192.168.***.***",6379);

        jedis.lpush("key1","lucy","mary","jack");
        List key = jedis.lrange("key1", 0, -1);
        System.out.println(key);

        Set keys = jedis.keys("*");
        for (String str: keys) {
            System.out.println(str);
        }
    }

    //操作key,String
    @Test
    public void demo1(){
        //创建Jedis对象
        Jedis jedis = new Jedis("192.168.***.***",6379);
        //添加
        jedis.set("name","lucy");
        //获取
        String name = jedis.get("name");
        System.out.println(name);

        //设置多个key-value
        jedis.mset("k1","v1","k2","v2");
        List mget = jedis.mget("k1", "k2");
        System.out.println(mget);


        Set keys = jedis.keys("*");
        for (String str: keys) {
            System.out.println(str);
        }
    }

案列:实现手机验证码功能

package com.zc.jedis;

import redis.clients.jedis.Jedis;

import java.util.Random;

public class PhoneCode {
    public static void main(String[] args) {
        //模拟验证码发送
        verfyCode("186");

        //测试验证码校验
        //getRedisCode("186","933938");
    }

    //3验证码校验
    public static void getRedisCode(String phone,String code){
        Jedis jedis = new Jedis("192.168.***.***",6379);
        //获取key
        String codeKey = "VerifyCode" + phone + ":code";
        String redisCode = jedis.get(codeKey);
        if (redisCode.equals(code)){
            System.out.println("成功");
        }else {
            System.out.println("失败");
        }
        jedis.close();
    }


    //2每个手机每天只能发送三次,验证码放在redis,设置过期时间
    public static void verfyCode(String phone){
        Jedis jedis = new Jedis("192.168.239.128",6379);
         //拼接key
        //手机发送次数key
        String countKey = "VerifyCode" + phone + ":count";
        //验证码key
        String codeKey = "VerifyCode" + phone + ":code";

        //每个手机每天只能发送三次
        String count = jedis.get(countKey);
        if (count == null){
            //没有发送次数,第一次发送
            //设置发送次数为1
            jedis.setex(countKey,24*60*60,"1");
        }else if(Integer.parseInt(count) <= 2){
            //发送次数加1
            jedis.incr(countKey);
        }else if (Integer.parseInt(count) > 2){
            //发送三次,不能再发送了
            System.out.println("今天发送次数已超过三次");
            return;
        }
        //发送验证码放到redis中
        jedis.setex(codeKey,120,getCode());
        jedis.close();
    }

    //1.生成6位随机验证码
    public static String getCode(){
        Random random = new Random();
        String code = "";
        for (int i = 0; i < 6; i++) {
            int rand = random.nextInt(10);
            code = code + rand;
        }
        return code;
    }
}

 

SpringBoot整合redis

1、在pom.xml文件中引入redis相关依赖



org.springframework.boot
spring-boot-starter-data-redis





org.apache.commons
commons-pool2
2.6.0

2、application.properties配置redis配置

#Redis服务器地址
spring.redis.host=192.168.140.136
#Redis服务器连接端口
spring.redis.port=6379
#Redis数据库索引(默认为0)
spring.redis.database= 0
#连接超时时间(毫秒)
spring.redis.timeout=1800000
#连接池最大连接数(使用负值表示没有限制)
spring.redis.lettuce.pool.max-active=20
#最大阻塞等待时间(负数表示没限制)
spring.redis.lettuce.pool.max-wait=-1
#连接池中的最大空闲连接
spring.redis.lettuce.pool.max-idle=5
#连接池中的最小空闲连接
spring.redis.lettuce.pool.min-idle=0

3、添加redis配置类

@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {

    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate template = new RedisTemplate<>();
        RedisSerializer redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        template.setConnectionFactory(factory);
//key序列化方式
        template.setKeySerializer(redisSerializer);
//value序列化
        template.setValueSerializer(jackson2JsonRedisSerializer);
//value hashmap序列化
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        return template;
    }

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        RedisSerializer redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
//解决查询缓存转换异常的问题
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
// 配置序列化(解决乱码的问题),过期时间600秒
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofSeconds(600))
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
                .disableCachingNullValues();
        RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .build();
        return cacheManager;
    }
}

4、测试:

@RestController
@RequestMapping("/redisTest")
public class RedisTestController {
    @Autowired
    private RedisTemplate redisTemplate;

    @GetMapping
    public String testRedis(){
        //设置值到Redis
        redisTemplate.opsForValue().set("name","lucy");
        String name = (String) redisTemplate.opsForValue().get("name");
        return name;
    }
}

案列: 秒杀案列

	//秒杀过程
	public static boolean doSecKill(String uid,String prodid) throws IOException {
		//1 uid和prodid非空判断
		if(uid == null || prodid == null) {
			return false;
		}

		//2 连接redis
		Jedis jedis = new Jedis("192.168.***.***",6379);
		//通过连接池得到jedis对象
//		JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance();
//		Jedis jedis = jedisPoolInstance.getResource();

		//3 拼接key
		// 3.1 库存key
		String kcKey = "sk:"+prodid+":qt";
		// 3.2 秒杀成功用户key
		String userKey = "sk:"+prodid+":user";

		//监视库存
		jedis.watch(kcKey);

		//4 获取库存,如果库存null,秒杀还没有开始
		String kc = jedis.get(kcKey);
		if(kc == null) {
			System.out.println("秒杀还没有开始,请等待");
			jedis.close();
			return false;
		}

		// 5 判断用户是否重复秒杀操作
		if(jedis.sismember(userKey, uid)) {
			System.out.println("已经秒杀成功了,不能重复秒杀");
			jedis.close();
			return false;
		}

		//6 判断如果商品数量,库存数量小于1,秒杀结束
		if(Integer.parseInt(kc)<=0) {
			System.out.println("秒杀已经结束了");
			jedis.close();
			return false;
		}

		//7 秒杀过程
		//使用事务
		Transaction multi = jedis.multi();

		//组队操作
		multi.decr(kcKey);
		multi.sadd(userKey,uid);

		//执行
		List results = multi.exec();

		if(results == null || results.size()==0) {
			System.out.println("秒杀失败了....");
			jedis.close();
			return false;
		}

		//7.1 库存-1
		//jedis.decr(kcKey);
		//7.2 把秒杀成功用户添加清单里面
		//jedis.sadd(userKey,uid);

		System.out.println("秒杀成功了..");
		jedis.close();
		return true;
	} 
  

并发模拟秒杀 :使用ab工具模拟测试

1.在uburtn中安装ab指令:sudo apt-get install apache2-utils

springBoot整合redis以及事务的处理_第1张图片

2.vim postfile 模拟表单提交参数,以&符号结尾;存放当前目录。

3.执行指令

并发下上述秒杀存在问题

连接超时

描述:2000个客户端参与秒杀,当前redis只提供一条连接路径,当有一个用户在限定时间内还没连上redis,会提示连接超时.

解决:使用连接池

public class JedisPoolUtil {
	private static volatile JedisPool jedisPool = null;

	private JedisPoolUtil() {
	}

	public static JedisPool getJedisPoolInstance() {
		if (null == jedisPool) {
			synchronized (JedisPoolUtil.class) {
				if (null == jedisPool) {
					JedisPoolConfig poolConfig = new JedisPoolConfig();
					poolConfig.setMaxTotal(200);
					poolConfig.setMaxIdle(32);
					poolConfig.setMaxWaitMillis(100*1000);
					poolConfig.setBlockWhenExhausted(true);
					poolConfig.setTestOnBorrow(true);  // ping  PONG
				 
					jedisPool = new JedisPool(poolConfig, "192.168.***.***", 6379, 60000 );
				}
			}
		}
		return jedisPool;
	}

	public static void release(JedisPool jedisPool, Jedis jedis) {
		if (null != jedis) {
			jedisPool.returnResource(jedis);
		}
	}

}

超卖

描述:假设此时库存只有1,第一个请求访问redis获取到库存数量(获取返回),然后返回逻辑代码中,在准备执行减库存时;第二个请求来读取到了数据,发现商品库存是大于0的(获取返回)。然后两者都会执行秒杀的逻辑,然而库存只有一个,就遇到了超卖的情况。

springBoot整合redis以及事务的处理_第2张图片

解决:使用redis内置乐观锁(watch)解决;在获取库存前,对要访问的key上锁.

springBoot整合redis以及事务的处理_第3张图片

库存遗留

描述:已经秒光,可是还有库存。

原因:我们知道,redis不支持回滚,一个操作,要么成功,要么失败(失败的操作,不会再次自动执行),乐观锁导致很多请求都失败

例如:有1到n个请求,由于乐观锁版本的变更,导致很多请求执行失败,导致最后成功执行的数量小于库存数量----库存遗留.

解决:使用Lua脚本,其具有一定原子性,不用加锁;将最开始的秒杀过程代码使用LUA脚本代替.

springBoot整合redis以及事务的处理_第4张图片

	static String secKillScript ="local userid=KEYS[1];\r\n" + 
			"local prodid=KEYS[2];\r\n" + 
			"local qtkey='sk:'..prodid..\":qt\";\r\n" + 
			"local usersKey='sk:'..prodid..\":usr\";\r\n" + 
			"local userExists=redis.call(\"sismember\",usersKey,userid);\r\n" + 
			"if tonumber(userExists)==1 then \r\n" + 
			"   return 2;\r\n" + 
			"end\r\n" + 
			"local num= redis.call(\"get\" ,qtkey);\r\n" + 
			"if tonumber(num)<=0 then \r\n" + 
			"   return 0;\r\n" + 
			"else \r\n" + 
			"   redis.call(\"decr\",qtkey);\r\n" + 
			"   redis.call(\"sadd\",usersKey,userid);\r\n" + 
			"end\r\n" + 
			"return 1" ;
			 
	static String secKillScript2 = 
			"local userExists=redis.call(\"sismember\",\"{sk}:0101:usr\",userid);\r\n" +
			" return 1";

	public static boolean doSecKill(String uid,String prodid) throws IOException {

		JedisPool jedispool =  JedisPoolUtil.getJedisPoolInstance();
		Jedis jedis=jedispool.getResource();

		 //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;
	}

分布式锁

随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的Java API并不能提供分布式锁的能力。为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!

分布式锁主流的实现方案:
1. 基于数据库实现分布式锁
2. 基于缓存(Redis等)
3. 基于Zookeeper 

redis实现分布式锁

springBoot整合redis以及事务的处理_第5张图片 

1. 多个客户端同时获取锁(setnx)
2. 获取成功,执行业务逻辑{从db获取数据,放入缓存},执行完成释放锁(del)
3. 其他客户端等待重试

@GetMapping("testLock")
public void testLock(){
    //1获取锁,setne
    Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111");
    //2获取锁成功、查询num的值
    if(lock){
        Object value = redisTemplate.opsForValue().get("num");
        //2.1判断num为空return
        if(StringUtils.isEmpty(value)){
            return;
        }
        //2.2有值就转成成int
        int num = Integer.parseInt(value+"");
        //2.3把redis的num加1
        redisTemplate.opsForValue().set("num", ++num);
        //2.4释放锁,del
        redisTemplate.delete("lock");

    }else{
        //3获取锁失败、每隔0.1秒再获取
        try {
            Thread.sleep(100);
            testLock();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

问题一:setnx刚好获取到锁,业务逻辑出现异常,导致锁无法释放
解决:设置过期时间,自动释放锁。

springBoot整合redis以及事务的处理_第6张图片

问题二:可能会释放其他服务器的锁。 

场景:如果业务逻辑的执行时间是7s。执行流程如下:

  1. index1业务逻辑没执行完,3秒后锁被自动释放。
  2. index2获取到锁,执行业务逻辑,3秒后锁被自动释放。
  3. index3获取到锁,执行业务逻辑
  4. index1业务逻辑执行完成,开始调用del释放锁,这时释放的是index3的锁,导致index3的业务只执行1s就被别人释放。

解决方法:setnx获取锁时,设置一个指定的唯一值(例如:uuid);释放前获取这个值,判断是否自己的锁。 

springBoot整合redis以及事务的处理_第7张图片

问题三:删除操作缺乏原子性 。

场景:

  1. index1业务执行完,经过uuid验证后准备释放锁,此时刚好锁到期,系统自动释放锁;
  2. index2获取到锁,执行业务;
  3. index1执行删除锁操作,刚好释放掉index2的锁。

 解决:使用Lua脚本

@GetMapping("testLockLua")
public void testLockLua() {
    //1 声明一个uuid ,将做为一个value 放入我们的key所对应的值中
    String uuid = UUID.randomUUID().toString();
    //2 定义一个锁:lua 脚本可以使用同一把锁,来实现删除!
    String skuId = "25"; // 访问skuId 为25号的商品 100008348542
    String locKey = "lock:" + skuId; // 锁住的是每个商品的数据

    // 3 获取锁
    Boolean lock = redisTemplate.opsForValue().setIfAbsent(locKey, uuid, 3, TimeUnit.SECONDS);

    // 第一种: lock 与过期时间中间不写任何的代码。
    // redisTemplate.expire("lock",10, TimeUnit.SECONDS);//设置过期时间
    // 如果true
    if (lock) {
        // 执行的业务逻辑开始
        // 获取缓存中的num 数据
        Object value = redisTemplate.opsForValue().get("num");
        // 如果是空直接返回
        if (StringUtils.isEmpty(value)) {
            return;
        }
        // 不是空 如果说在这出现了异常! 那么delete 就删除失败! 也就是说锁永远存在!
        int num = Integer.parseInt(value + "");
        // 使num 每次+1 放入缓存
        redisTemplate.opsForValue().set("num", String.valueOf(++num));
        /*使用lua脚本来锁*/
        // 定义lua 脚本
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        // 使用redis执行lua执行
        DefaultRedisScript redisScript = new DefaultRedisScript<>();
        redisScript.setScriptText(script);
        // 设置一下返回值类型 为Long
        // 因为删除判断的时候,返回的0,给其封装为数据类型。如果不封装那么默认返回String 类型,
        // 那么返回字符串与0 会有发生错误。
        redisScript.setResultType(Long.class);
        // 第一个要是script 脚本 ,第二个需要判断的key,第三个就是key所对应的值。
        redisTemplate.execute(redisScript, Arrays.asList(locKey), uuid);
    } else {
        // 其他线程等待
        try {
            // 睡眠
            Thread.sleep(1000);
            // 睡醒了之后,调用方法。
            testLockLua();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

你可能感兴趣的:(redis,spring,boot,java)