Redis——整合springboot

redis6新的数据类型——HyperLogLog

Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。

解决基数问题有很多种方案:

  1. 数据存储在MySQL表中,使用distinct count计算不重复个数

  2. 使用Redis提供的hash、set、bitmaps等数据结构来处理

以上的方案结果精确,但随着数据不断增加,导致占用空间越来越大,对于非常大的数据集是不切实际的。

什么是基数?

比如数据集 {1, 3, 5, 7, 5, 7, 8}, 那么这个数据集的基数集为 {1, 3, 5 ,7, 8}, 基数(不重复元素)为5。 基数估计就是在误差可接受的范围内,快速计算基数。


HyperLogLog常用方法

  • pfadd :可以添加一个或多个的元素。这些元素如果重复则不会添加成功。如果一个添加不成功,剩下的也会添加成功
pfadd k1 java c++ java
# 输出结果为1,表示添加成功
pfcount k1
# 输出结果为2,第二次java没有添加进去
  • pfcount
  • pfmerge [sourcekey...]:将一个或多个HLL合并后的结果存储在另一个HLL中,比如每月活跃用户可以使用每天的活跃用户来合并计算可得
pfadd k1 java c++ mysql
pfadd k2 c python mysql
pfmerge k3 k1 k2
pfcount k3
# 输出结果为5,重复的mysql被去重。

redis6新的数据类型——Geospatial

Redis 3.2 中增加了对GEO类型的支持。GEO,Geographic,地理信息的缩写。该类型,就是元素的2维坐标,在地图上就是经纬度。redis基于该类型,提供了经纬度设置,查询,范围查询,距离查询,经纬度Hash等常见操作。

  • geoadd<longitude经度><latitude纬度>:添加一个或多个经纬度组成的……城市?
geoadd china:city 121.47 31.23 shanghai
geoadd china:city 106.50 29.53 chongqing 
#也可以写在一起
geoadd china:city 114.05 22.52 shenzhen 116.38 39.90 beijing
  • geopos [member...]: 获得指定地区的坐标值
geopos china:city shanghai
# 输出经纬度
  • geodist :获取两个位置之间的直线距离。可以指定单位,默认为m
geodist china:city shenzhen beijing km
# 输出结果为1945.5740
  • georadius :给定的经纬度为中心,找出某一半径内的元素
georadius china:city 110 30 1000 km
# 找出经度110 纬度30 为中心1000km为半径在china:city中的元素
# 输出结果为shenzhen

我觉得了解一哈就行了。。


Jedis

使用java代码操作redis数据库

  • 引入依赖
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.2.0</version>
</dependency>
  • 编写代码
/**
* 1.需要将redis仅本机访问关闭
* 2.需要将保护模式关闭
*/
public static void main(String[] args) {
    //创建jedis对象
    Jedis jedis = new Jedis("192.168.1.2",6379);
    String value= jedis.ping();
    System.out.println(value);//输出结果为PONG
}

此时的jedis可以使用以上所有的命令。


Jedis操作字符串

@Test
public void testDemo1(){
    jedis.mset("k1","v1","k2","v2");
    List<String> mget = jedis.mget("k1", "k2");
    for (String s : mget) {
        System.out.println(s);
    }
}
//输出结果为 v1 v2

Jedis操作列表

@Test
public void testDemo2(){
    jedis.flushDB();
    jedis.lpush("name","boerk","lyly","cookie");
    List<String> name = jedis.lrange("name", 0, -1);
    System.out.println(name);
}
//输出结果为[cookie, lyly, boerk]

Jedis操作set

/**
* set是一个自动排重的无序集合
*/
@Test
public void testDemo3(){
    jedis.flushDB();
    jedis.sadd("name","boerk","lyly","cookie");
    Set<String> name = jedis.smembers("name");
    System.out.println(name);
}
//输出结果为[lyly, cookie, boerk]

Jedis操作hash

/**
* hash存储的是一个个键值对
*/
@Test
public void testDemo4(){
    jedis.flushDB();
    jedis.hset("user","age","20");
    String hget = jedis.hget("user", "age");
    System.out.println(hget);
}
/**
 * 如果需要使用hmset一次性存储多个值,则需要传入HashMap
 */
@Test
public void testDemo4(){
    jedis.flushDB();
    HashMap hashMap = new HashMap();
    hashMap.put("jiangsu","nanjing");
    hashMap.put("anhui","hefei");
    jedis.hmset("provincial_capital",hashMap);
    Map<String, String> provincial_capital = jedis.hgetAll("provincial_capital");
    System.out.println(provincial_capital);
}

Jedis操作Zset

/**
 * Zset是一个关联了评分的简单字符串集合
 */
@Test
public void testDemo6(){
    jedis.flushDB();
    //添加一个
    jedis.zadd("name",500d,"java");
    //添加多个使用hashmap<String,Double>,添加顺序是反过来的…
    HashMap hashMap = new HashMap();
    hashMap.put("c++",600d);
    hashMap.put("mysql",700d);
    jedis.zadd("name",hashMap);
    Set<String> name = jedis.zrange("name", 0, -1);
    System.out.println(name);
}

如果需要使用WITHSCORES,则使用

Set<Tuple> name = jedis.zrangeWithScores("name", 0, -1);
System.out.println(name);
//输出结果为[[java,500.0], [c++,600.0], [mysql,700.0]]

Redis案例:

  1. 输入手机号,点击发动后随机生成6位数字码,2分钟有效

  2. 输入验证码,点击验证,返回成功或失败

  3. 每个手机,每天只能输入三次

public class demo2 {
        /**
         * 1.输入手机号,点击发动后随机生成6位数字码,2分钟有效
         * 2.输入验证码,点击验证,返回成功或失败
         * 3.每个手机,每天只能输入三次
         */
    static Jedis jedis = new Jedis("192.168.1.2",6379);
    public static void main(String[] args) {
        String tel="11111111111";
        //        //存储
        //        verify(tel);
        //验证
        isSame(tel,"111111");
    }
    public static String getCode(){
        Random random = new Random();
        String code = "";
        for (int i=0;i<=5;i++){
            int randomCode = random.nextInt(10);
            code+=randomCode;
        }
        return code;
    }
    //每个手机号,每天只能发送三次
    public static void verify(String tel){
        String s = jedis.get(tel);
        if (s==null){
            jedis.setex(tel,24*60*60,"1");
        }else if (Integer.parseInt(s)<=2){
            jedis.incr(tel);
        }else {
            System.out.println("今天已经获取三次了!请明天再试");
            jedis.close();
            return;
        }
        //2.没有超过三次就获取验证码。
        String code = getCode();
        //存储验证码
        String telCode=tel+"code";
        save(telCode,code);
    }
    //3.储存
    public static void save(String tel,String code){
        jedis.setex(tel,120,code);
        jedis.close();
    }
    //4.检验验证码是否相同
    public static void isSame(String tel,String code){
        String telCode=tel+"code";
        String s = jedis.get(telCode);
        if (code.equals(s)){
            System.out.println("正确!");
        }else {
            System.out.println("错误!");
        }
    }
}


SpringBoot整合redis

  • 引入依赖
<!-- redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- spring2.X集成redis所需common-pool2-->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
    <version>2.6.0</version>
</dependency>
<!--这个依赖不引入,PropertyAccessor.ALL会报错-->
<dependency>
    <groupId>com.fasterxml.jackson</groupId>
    <artifactId>jackson-base</artifactId>
    <version>2.13.2</version>
    <type>pom</type>
</dependency>
  • 编写springboot配置类
@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        RedisSerializer<String> 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<String> 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;
    }
}
  • 测试
@RestController
public class RedisController {
    @Autowired
    RedisTemplate redisTemplate;
    @GetMapping("/testRedis")
    public String testRedis(){
        redisTemplate.opsForValue().set("name","kitty");
        return (String)redisTemplate.opsForValue().get("name");
    }
}

RedisTemplate中定义了对5种数据结构操作

redisTemplate.opsForValue();//操作字符串
redisTemplate.opsForHash();//操作hash
redisTemplate.opsForList();//操作list
redisTemplate.opsForSet();//操作set
redisTemplate.opsForZSet();//操作有序set

Redis事务操作

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

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

  • Multi:开启事务,进入组队阶段。
  • Exec:执行命令,进入执行阶段。
  • Discard:弃用

使用multi后,所有的命令不会立即执行,而是等待exec。使用discard后,命令就被弃用了。

  • 在组队阶段,命令出现错误,而执行(exec),则所有的命令都会失效
  • 在执行阶段,命令出现错误,则该命令失效,其他命令正常进行。

事务冲突

image

  • 悲观锁
    image

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

但是效率会很低!!!

  • 乐观锁

image

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


Redis模拟乐观锁

  • watch
set balance 100
watch balance
multi
incrby balance 50
---此时另一窗口
incrby balance 500
#返回值为600
---
exer
#返回值为nil

Redis事务的三特性

  • 单独的隔离操作

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

  • 没有隔离级别的概念

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

  • 不保证原子性

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


案例:秒杀。

//秒杀过程
	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.44.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<Object> 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;
	}

使用LUA脚本语言解决乐观锁的库存遗留问题


posted @ 2022-03-14 09:10  Boerk  阅读(73)  评论(0)    收藏  举报