Redis之String应用场景与SpringCache--存储对象信息、分布式唯一ID、文章阅读量、Lua脚本_stringrediscache

img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

    User user=operations.get(key);
    
    //2.redis没有的话,直接去数据库捞
    if(user==null){
        user=this.userMapper.selectByPrimaryKey(userid);
        //由于redis没有才到数据库捞,所以必须把捞到的数据写入redis,方便下次查询能redis命中。
        operations.set(key,user);
    }
    return user;
}

}


步骤体验效果:  
 用http://127.0.0.1:9090/swagger-ui.html# 体验  
 问题1:进redis的数据必须序列化Serializable


问题2:如果连接不了redis



vi redis.conf
bind 0.0.0.0


### 重写Redis序列


默认情况下,Redis序列化使用的JDK序列化方式JdkSerializationRedisSerializer,这就会导致产生两个问题:


1. 被序列化的对象必须实现Serializable接口;



@Table(name = “users”)
public class User implements Serializable {…}


2. 被序列化会出现乱码,导致value值可读性差



127.0.0.1:6379> keys *

  1. “\xac\xed\x00\x05t\x00\auser:62”
  2. “\xac\xed\x00\x05t\x00\auser:65”
  3. “\xac\xed\x00\x05t\x00\auser:50”
  4. “\xac\xed\x00\x05t\x00\auser:36”
  5. “\xac\xed\x00\x05t\x00\x06user:6”
  6. “\xac\xed\x00\x05t\x00\auser:17”
  7. “\xac\xed\x00\x05t\x00\auser:28”

127.0.0.1:6379> get “\xac\xed\x00\x05t\x00\auser:62”
“\xac\xed\x00\x05sr\x00\x1acom.agan.redis.entity.User?\xebU\xa1\xe2\xa6\xfe\xe3\x02\x00\aL\x00\ncreateTimet
\x00\x10Ljava/util/Date;L\x00\adeletedt\x00\x10Ljava/lang/Byte;L\x00\x02idt\x00\x13Ljava/lang/Integer;L\x00
\bpasswordt\x00\x12Ljava/lang/String;L\x00\x03sexq\x00\x00\x02L\x00\nupdateTimeq\x00\x00\x01L\x00\buser
nameq\x00~\x00\x04xpsr\x00\x0ejava.util.Datehj\x81\x01KYt\x19\x03\x00\x00xpw\b\x00\x00\x01o+5\x1d\xf8xsr
\x00\x0ejava.lang.Byte\x9cN`\x84\xeeP\xf5\x1c\x02\x00\x01B\x00\x05valuexr\x00\x10java.lang.Number\x86\xac
\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00sr\x00\x11java.lang.Integer\x12\xe2\xa0\xa4\xf7\x81\x878\x02\x00
\x01I\x00\x05valuexq\x00\x00\t\x00\x00\x00>t\x00\x04un59q\x00\x00\nsq\x00~\x00\x06w\b\x00\x00\x01o+5\x1d
\xf8xt\x00\x04un59”


获取的值都是乱码。


#### 解决方式



@Configuration
public class RedisConfiguration {
/**
* 重写Redis序列化方式,使用Json方式:
* 当我们的数据存储到Redis的时候,我们的键(key)和值(value)都是通过Spring提供的Serializer序列化到Redis的。
* RedisTemplate默认使用的是JdkSerializationRedisSerializer,
* StringRedisTemplate默认使用的是StringRedisSerializer。
*
* Spring Data JPA为我们提供了下面的Serializer:
* GenericToStringSerializer、Jackson2JsonRedisSerializer、
* JacksonJsonRedisSerializer、JdkSerializationRedisSerializer、
* OxmSerializer、StringRedisSerializer。
* 在此我们将自己配置RedisTemplate并定义Serializer。
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
//创建一个json的序列化对象
GenericJackson2JsonRedisSerializer jackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
//设置value的序列化方式json
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
//设置key序列化方式string
redisTemplate.setKeySerializer(new StringRedisSerializer());

    //设置hash key序列化方式string
    redisTemplate.setHashKeySerializer(new StringRedisSerializer());
    //设置hash value的序列化方式json
    redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
    redisTemplate.afterPropertiesSet();
    return redisTemplate;
}

}


测试:



  1. 先把user的序列化删除
  2. 创建类RedisConfiguration
  3. flushdb 清空redis的旧数据,因为改了序列化,老数据以及不能兼容了,必须清空旧数据
  4. 往redis 初始化100条数据
  5. 用 keys * 命令查看所有key

127.0.0.1:6379> keys \*


1. “user:187”
2. “user:117”
3. “user:170”
4. “user:139”
5. “user:157”


127.0.0.1:6379> get user:187  
 “{”@class":“com.agan.redis.entity.User”,“id”:187,“username”:“un84”,“password”:“un84”,  
 “sex”:0,“deleted”:0,“updateTime”:[“java.util.Date”,1576983528000],  
 “createTime”:[“java.util.Date”,1576983528000]}"



总结

  1. 对于Redis的存储对象信息,其实就是 redisTemplate.opsForValue().set(key,value)就可以解决
  2. 对于Redis,DB操作顺序问题,一般都是先操作DB,再操作Redis,尽可能避免产生脏数据。
  3. 如果先更新Redis,再更新DB,如果更新DB失败,那么Redis数据就是脏数据。
  4. 由于Redis使用了JDK序列化方式,对象需要实现序列化接口,Redis存储的值有乱码问题,可读性差,所以需要设置Redis key,value的序列化方式。

SpringCache

  • SpringCache 他是对使用缓存进行封装和抽象,通过在方法上使用annotation注解就能拿到缓存结果;
  • 用了Annotation解决了业务代码和缓存代码的耦合度问题,即在不侵入业务代码的基础上让现有代码支持缓存;
  • 开发人员无感知使用了缓存
  • 特别注意:(注意:对于redis的缓存,springcache只支持String,其他的Hash 、List、set、ZSet都不支持,要特别注意)

代码

POM依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--spring cache-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!--spring cache连接池依赖包-->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
    <version>2.6.2</version>
</dependency>

配置文件

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

开启缓存配置,设置序列化

@Configuration
@EnableCaching
public class RedisConfig {
	@Primary
	@Bean
	public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory){
		RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
		redisCacheConfiguration = redisCacheConfiguration
				//设置缓存的默认超时时间:30分钟
				.entryTtl(Duration.ofMinutes(30L))
				//如果是空值,不缓存
				.disableCachingNullValues()

				//设置key序列化器
				.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(keySerializer()))
				//设置value序列化器
				.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(valueSerializer()));

		return RedisCacheManager
				.builder(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory))
				.cacheDefaults(redisCacheConfiguration)
				.build();
	}

	/\*\*
 \* key序列化器
 \*/
	private RedisSerializer<String> keySerializer() {
		return new StringRedisSerializer();
	}
	/\*\*
 \* value序列化器
 \*/
	private RedisSerializer<Object> valueSerializer() {
		return new GenericJackson2JsonRedisSerializer();
	}

}

逻辑代码:

@Api(description = "用户接口")
@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    private UserService userService;
    @ApiOperation("单个用户查询,按userid查用户信息")
    @RequestMapping(value = "/findById/{id}", method = RequestMethod.GET)
    public UserVO findById(@PathVariable int id) {
        User user = this.userService.findUserById(id);
        UserVO userVO = new UserVO();
        BeanUtils.copyProperties(user, userVO);
        return userVO;
    }

    @ApiOperation("修改某条数据")
    @PostMapping(value = "/updateUser")
    public void updateUser(@RequestBody UserVO obj) {
        User user = new User();
        BeanUtils.copyProperties(obj, user);
        userService.updateUser(user);
    }

    @ApiOperation("按id删除用户")
    @RequestMapping(value = "/del/{id}", method = RequestMethod.GET)
    public void deleteUser(@PathVariable int id) {
        this.userService.deleteUser(id);
    }

}

Service

@Service
@CacheConfig(cacheNames = { "user" })
public class UserService {

    private static final Logger LOGGER = LoggerFactory.getLogger(UserService.class);

    @Autowired
    private UserMapper userMapper;

    @Cacheable(key="#id")
    public User findUserById(Integer id){
        return this.userMapper.selectByPrimaryKey(id);
    }

    @CachePut(key = "#obj.id")
    public User updateUser(User obj){
        this.userMapper.updateByPrimaryKeySelective(obj);
        return this.userMapper.selectByPrimaryKey(obj.getId());
    }

    @CacheEvict(key = "#id")
    public void deleteUser(Integer id){
        User user=new User();
        user.setId(id);
        user.setDeleted((byte)1);
        this.userMapper.updateByPrimaryKeySelective(user);
    }

}

剖析SpringCache常用注解
@CacheConfig
  • @CacheConfig是类级别的注解,同意该类的所有缓存都可以作为前缀;
  • @CacheConfig(cacheNames={"product}) 代表该类的所有缓存都是 product:: 为前缀;
@Cacheable
  • @Cacheable是方法级别的注解,拥有将方法的结果缓存起来;
    方法被调用时,先从缓存中读取数据,如果缓存中不存在,再执行方法体,查询到值后,把值放入缓存中;
@Cacheable(key="#id")
public User findUserById(Integer id){
    return this.userMapper.selectByPrimaryKey(id);
}

  • 一般情况下,@CacheConfig和@Cacheable是搭配使用的。
  • 如果传入的值为1000,则key为user::1000;
@CachePut
  • @CachePut是方法级别的注解,用于更新缓存;
  • 当方法被调用时,先执行方法体,然后springcache通过返回值更新缓存;
@CachePut(key = "#obj.id")
public User updateUser(User obj){
    this.userMapper.updateByPrimaryKeySelective(obj);
    return this.userMapper.selectByPrimaryKey(obj.getId());
}

@CacheEvict(key = “#id”)
  • 是方法级别的注解,用于删除缓存;
  • 一般删除缓存涉及到两种操作:一种是更新DB数据后,删除Redis数据,另一种是删除DB数据后,删除Redis数据;
  • 当方法被调用时,先执行方法体,通过方法参数删除缓存;
  @CacheEvict(key = "#id")
    public void deleteUser(Integer id){
        User user=new User();
        user.setId(id);
        user.setDeleted((byte)1);
        this.userMapper.updateByPrimaryKeySelective(user);
    }

springcache坑
  • 对于Redis缓存,只支持String类型,其他类型不支持;
  • 对于多表查询数据,SpringCache不支持,只支持单表简单缓存;多表用RedisTemplate;

阅读量操作

像日常操作中,热点新闻阅读量、贴吧帖子阅读量、文章阅读量,只要用户查看了这些东西,其阅读量对应+1,大的并发量,一般不可能采用数据库来做计数器,通常都是用redis的incr命令来实现。

redis incr

用途就是计数器,如果key不存在,那就将key的value值初始化为0,如果存在,则自动加1;

127.0.0.1:6379> incr article:100
(integer) 1
127.0.0.1:6379> incr article:100
(integer) 2
127.0.0.1:6379> incr article:100
(integer) 3
127.0.0.1:6379> incr article:100
(integer) 4
127.0.0.1:6379> get article:100
"4"

技术方案的缺陷:
需要频繁的修改redis,耗费CPU,高并发修改redis会导致 redisCPU 100%

代码实现

@RestController
@Slf4j
public class ViewController {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @GetMapping(value = "/view")
    public void view(Integer id) {
        //redis key
        String key="article:"+id;
        //调用redis的increment计数器命令
        long n=this.stringRedisTemplate.opsForValue().increment(key);
        log.info("key={},阅读量为{}",key, n);
    }
}

分布式唯一ID

  • 对于传统的单库单表,是不需要的,因为只需要自增就行;
  • 而架构升级为分布式系统后,集群中一般是存在分库分表操作,存在像Product1、Product2…ProductN张表,每张表都从1自增,显然不合理,在这么多张表中,不能存在ID相同的数据,因此传统的自增ID失去意义。
  • 故需要全局唯一的ID来标识每一条数据

分布式唯一ID特点

  • 全局唯一性;不能出现重复的ID;
  • 趋势递增;防止数据库索引底层数据结构B+树产生高频率的分裂、旋转操作、耗费服务器性能;
  • 信息安全;防止恶意用户窥见表数据;

大型分布式系统架构中,全局唯一ID生成器的机器需要实现高可用高QPS,不然整个系统就挂了;

分布式唯一ID的方案

  1. 分布式雪花算法
  2. UUID
  3. 美团Leaf算法
  4. Redis生成ID算法
  • 基于Redis INCR 命令生成 分布式全局唯一id
  • Redis 的INCR命令具备了"INCR AND GET"原子操作,即增加并返回结果的原子操作;
  • redis的单进程单线程架构,INCR命令不会出现ID重复
代码与思路

技术思路:

  1. 采用redis的INCR的命令,从1自增生成ID。
  2. 由于淘宝的商品面向全世界的海量商品,故 必须对其进行分库分表,每张表的id不能用自增,由redis的incr命令来自动生成。
  3. 淘宝的海量数据,分库分表分为1024张表,例如商品表product_0,product_1,product_2…product_1023

代码:
ID生成器代码类:

@Service
public class IdGenerator {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    private static final String ID_KEY = "id:generator:product";

    /\*\*
 \* 生成全局唯一id
 \*/
    public Long incrementId() {
        long n=this.stringRedisTemplate.opsForValue().increment(ID_KEY);
        return n;
    }

}

controller

@RestController
@Slf4j
@RequestMapping(value = "/pruduct")
public class ProductController {

    @Autowired
    private IdGenerator idGenerator;

    @PostMapping(value = "/create")
    public void create(Product obj) {
        //步骤1:生成分布式id
        long id=this.idGenerator.incrementId();
        //全局id,代替数据库的自增id
        obj.setId(id);

        //步骤2:取模,计算表名
        //类似于海量的数据,例如淘宝一般是分为1024张表,这里为了演示方便,只分为8张表。
        int table=(int)id % 8;
        String tablename="product_"+table;

        log.info("插入表名{},插入内容{}",tablename,obj);
    }
}

Lua脚本

Lua 是一个简洁、轻量、可扩展的脚本语言,它的特性有:

  • 轻量:源码包只有核心库,编译后体积很小。
  • 高效:由C 编写的,启动快、运行快。
  • 内嵌:可内嵌到各种编程语言或系统中运行,提升静态语言的灵活性。
    而且完全不需要担心语法问题,Lua 的语法很简单,分分钟使用不成问题。

Redis 为什么要使用LUA

  1. 原子性:将redis的多个操作合成一个脚本,然后整体执行,在脚本的执行中,不会出现资源竞争的情况。
  2. 减少网络通信:把多个命令合成一个lua脚本,redis统一执行脚本。
  3. 复用性:client发送的脚本会永久存储在redis中,这意味着其他客户端可以复用这一脚本来完成同样的逻辑。
lua的语法入门
EVAL script numkeys key [key ...] arg [arg ...]

  • script: 参数是一段 Lua脚本程序。脚本不必(也不应该)定义为一个Lua函数。
  • numkeys: 用于指定key参数的个数。
  • key [key …]: 代表redis的key,从 EVAL 的第三个参数开始算起,表示在脚本中所用到的Redis键(key)。
    在Lua中,这些键名参数可以通过全局变量 KEYS 数组,用1为基址的形式访问( KEYS[1] ,KEYS[2],依次类推)。
  • arg [arg …]: 代表lua的入参,在Lua中通过全局变量ARGV数组访问,访问的形式和KEYS变量类似( ARGV[1] 、 ARGV[2] ,诸如此类)。
  • 特别注意:lua的数组坐标不是从0开始,是从1开始。从1开始!从1开始!从1开始!
127.0.0.1:6379> EVAL  "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2  key1 key2 agan1 agna2
1) "key1"
2) "key2"
3) "agan1"
4) "agna2"

  1. eval为redis的关键字
  2. 双引号的内容代表lua脚本
  3. 2代表numkeys参数的个数,即有多少个key
  4. key1 和 key2代表 KEYS[1],KEYS[2]的入参
  5. agan1 agna2 是ARGV[1],ARGV[2]的入参

被优化的代码

 @GetMapping(value = "/updateuser")
    public void updateUser(Integer uid,String uname) {
        String key="user:"+uid;
        //优化点:第一次发送redis请求


![img](https://img-blog.csdnimg.cn/img_convert/4db3cce8ad9148d285d9211848671b32.png)
![img](https://img-blog.csdnimg.cn/img_convert/5e960490be9233aab3e04014c26270a5.png)

**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化的资料的朋友,可以添加戳这里获取](https://bbs.csdn.net/topics/618668825)**


**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**

为redis的关键字
2. 双引号的内容代表lua脚本
3. 2代表numkeys参数的个数,即有多少个key
4. key1 和 key2代表 KEYS[1],KEYS[2]的入参
5. agan1 agna2 是ARGV[1],ARGV[2]的入参


### 被优化的代码



@GetMapping(value = “/updateuser”)
public void updateUser(Integer uid,String uname) {
String key=“user:”+uid;
//优化点:第一次发送redis请求

[外链图片转存中…(img-hVdX5raP-1715841641225)]
[外链图片转存中…(img-S7PCzB1l-1715841641226)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值