皆非的万事屋 - redis
type: channel
link: http://makeyourchoice.cn/tag/redis/
items: redis知识总结
items: 通过Redis的自增set实现“乐观锁”，从而控制并发
items: Mybatis结合Redis缓存以注解开发时的查询优化方案——通过AOP和反射重新实现@One、@Many注解

redis知识总结
type: item
date: 2021-08-12T02:00:00+00:00
description: 内容转载自JavaGuide 介绍 简单介绍一下 Redis 简单来说 Redis 就是一个使用C 语言开发的数据库，不过与传统数据库不同的是 Redis 的数据是存在内存中的 ，也就是它是内存数据库，所以读写速度非常快，因此 Redis 被广泛应用于缓存方向。 另外，Redis 除了做缓存之外，也经常用来做分布式锁，甚至是消息队列。 Redis 提供了多种数据类型来支持不同的业务场景。Redis 还支持事务、持久化、Lua 脚本、多种集群方案。 分布式缓存方案 分布式缓存的话，使用比较多的主要是 Memcached 和 Redis 共同点： 都是基于内存的数据库，一般都用来当做缓存使用。 都有过期策略。 两者的性能都非常高。 区别： Redis 支持更丰富的数据类型（支持更复杂的应用场景）。Redis 不仅仅支持简单的 k/v 类型的数据，同时还提供 list，set，zset，hash 等数据结构的存储。Memcached 只支持最简单的 k/v 数据类型。 Redis 支持数据的持久化，可以将内存中的数据保持在磁盘中，重启的时候可以再次加载进行使用,而 Memecache 把数据全部存在内存之中。 Redis 有灾难恢复机制。 因为可以把缓存中的数据持久化到磁盘上。 Redis 在服务器内存使用完之后，可以将不用的数据放到磁盘上。但是，Memcached 在服务器内存使用完之后，就会直接报异常。 Redis 目前是原生支持 cluster 模式。Memcached 没有原生的集群模式，需要依靠客户端来实现往集群中分片写入数据 Redis 使用单线程的多路 IO 复用模型。Memcached 是多线程，非阻塞 IO 复用的网络模型。 Redis 支持发布订阅模型、Lua 脚本、事务等功能，而 Memcached 不支持。并且，Redis 支持更多的编程语言。 Memcached 过期数据的删除策略只用了惰性删除，而 Redis 同时使用了惰性删除与定期删除。 为什么使用redis 保证用户下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存，所以速度相当快。 一般像 MySQL 这类的数据库的 QPS 大概都在 1w 左右（4 核 8g） ，但是使用 Redis 缓存之后很容易达到 10w+，甚至最高能达到 30w+（就单机 redis 的情况，redis 集群的话会更高）。 redis的其他用途 分布式锁: 通过 Redis 来做分布式锁是一种比较常见的方式。通常情况下，我们都是基于 Redisson来实现分布式锁。 限流 ：一般是通过 Redis + Lua 脚本的方式来实现限流。 消息队列：Redis 自带的 list 数据结构可以作为一个简单的队列使用。Redis5.0 中增加的 Stream 类型的数据结构更加适合用来做消息队列。它比较类似于 Kafka，有主题和消费组的概念，支持消息持久化以及 ACK 机制。 复杂业务场景 ：通过 Redis 以及 Redis 扩展（比如 Redisson）提供的数据结构，我们可以很方便地完成很多复杂的业务场景比如通过 bitmap 统计活跃用户、通过 sorted set 维护排行榜。 redis中的数据结构 键key常用命令 keys * //查看当前库所有key (匹配：keys *1) exists key //判断某个key是否存在 type key //查看你的key是什么类型 del key //删除指定的key数据 unlink key //根据value选择非阻塞删除，仅将keys从keyspace元数据中删除，真正的删除会在后续异步操作。 expire key 10 //10秒钟：为给定的key设置过期时间 ttl key //查看还有多少秒过期，-1表示永不过期，-2表示已过期 select //命令切换数据库 dbsize //查看当前数据库的key的数量 flushdb //清空当前库 flushall //通杀全部库 String 介绍：string 数据结构是简单的 key-value 类型。 String类型是二进制安全的。意味着Redis的string可以包含任何数据。比如jpg图片或者序列化的对象。 虽然 Redis 是用 C 语言写的，但是 Redis 并没有使用 C 的字符串表示，而是自己构建了一种 简单动态字符串(Simple Dynamic String,缩写SDS)。 是可以修改的字符串，内部结构实现上类似于Java的ArrayList，采用预分配冗余空间的方式来减少内存的频繁分配。 当字符串长度小于1M时，扩容都是加倍现有的空间，如果超过1M，扩容时一次只会多扩1M的空间。一个Redis中字符串value最…

通过Redis的自增set实现“乐观锁”，从而控制并发
type: item
date: 2020-11-20T01:51:00+00:00
description: [scode type="blue"]这还要从项目提出的静默签到开始讲起[/scode] 学长说我们的新院统战要对用户的登录进行记录，也就是签到，但是不是用户手动去页面点击签到，而是用户今天登录了，或者说向服务器发送请求了，那么就算是签过到了，数据库有他今天的登录记录。 emmm，一开始还真是让我头大，因为不是新写一个接口去进行签到，而是通过请求进行签到，一天只能签一次，那么首先这个判断的位置只能是放在过滤器中，当用户请求进来时，拦截下来，拿token，获取用户id。 ok，拿到用户id了，但是不能去通过查数据库来检测用户是否这一天已经登陆过，因为慢，而且请求是时时刻刻都有的，查数据库显然不理想，那肯定就是去查redis里有没有今天的记录，把userId当作key，value随意，就有了一开始的写法： //如果缓存里没有 if (!redisUtils.hasKey(key)) { //写入数据库 ... //写入缓存，第二天凌晨过期 redisUtils.set(key, value, DateUtil.getTomorrowSeconds(new Date())); } 一开始这样写，还没有什么太大的问题，但是之后就发现了数据库第一时刻记录了多条用户登录记录，经过排查之后，可以初步判定是多条请求同时进来，而 if (!redisUtils.hasKey(key)) 需要时间并没有全部拦截，所以上面的写法是有问题的。 那我就想了，既然redis读取有延时，我直接用 ConcurrentHashMap 来记录做外层第一次拦截,同时写一个定时任务清空，同时里面再加第二层判断拦截记录到redis（考虑到服务器重启），这样延迟足够小了吧，应该可以拦下并发的请求访问的，可结果告诉我还是不行，即使是用了map也拦截不全。 [scode type="blue"]通过redis的自增set做乐观锁[/scode] 现在可以明确是要控制并发访问了，这一块我之前有写过悲观锁和乐观锁，但很明显这个不能使用悲观锁，太慢了。那就使用校验字段来加乐观锁，通过查阅知道redis是单线程的，所以在写入操作一定是线程安全的！那么就要使用到redis的自增set了： /** * 缓存自增放入（乐观锁） * * @param key 键 * @param value 自增大小（Long） * @return 返回已增长度 */ public Long setnc(String key, Object value) { try { return redisTemplate.opsForValue().increment(key, (Long)value); } catch (Exception e) { log.error(e.getMessage(), e); return 0L; } } 这个可以保证每一次进入的请求都会增加该条缓存的增量记录值，由于redis是线程安全的，所以可以保证每个请求最后返回的自增长度是不同的，那么怎们就对第一次自增后的长度做一个判断： //如果自增长度小于等于1（只有第一次，后面的都会越来越大） if (redisUtils.setnc(key, 1L) <= 1L) { //存入数据库... //设置过期时间 redisUtils.expire(SIGN_KEY + userId, DateUtil.getTomorrowSeconds(new Date())); } 经过测试，可以控制redis的并发访问所带来的差错，这样，静默签到算是完成了。

Mybatis结合Redis缓存以注解开发时的查询优化方案——通过AOP和反射重新实现@One、@Many注解
type: item
date: 2020-10-23T01:14:00+00:00
description: 前言 [scode type="share"]现在算是终于有空来写这篇文章了，由于考研的问题一直没有时间来补这篇文章[/scode] 该问题最早提出是在9月份开发新院统战项目时提出的，也就是我刚刚搭完第一版后端框架，其中持久层使用的框架是mybatis plus，缓存使用的是sprinboot缓存注解+redis，其中将缓存注解加在service层。 当时在使用mybatis plus并以注解开发时，由于过度使用@One与@Many注解进行属性的封装，例如： @Select("SELECT id,dept_id,username,nick_name,gender,phone,email,avatar_path,password," + "is_admin,enabled,create_by,update_by,pwd_reset_time,create_time,update_time" + " FROM sys_user u WHERE u.id = #{id} AND is_deleted=0") @Results({ @Result(column = "id", property = "id"), @Result(column = "dept_id", property = "deptId"), @Result(column = "dept_id", property = "dept", one = @One(select = "marchsoft.modules.system.mapper.DeptMapper.selectById", fetchType = FetchType.EAGER)), @Result(column = "id", property = "roles", many = @Many(select = "marchsoft.modules.system.mapper.RoleMapper.findWithMenuByUserId", fetchType = FetchType.EAGER)), @Result(column = "id", property = "jobs", many = @Many(select = "marchsoft.modules.system.mapper.JobMapper.findByUserId", fetchType = FetchType.EAGER)) }) UserBO findUserDetailById(Long id); 但是没有考虑到一句@One就是一条新的sql，一句@Many就又是n条sql，在没有缓存的情况下效率是非常低的，这就牵扯到之后的不断探究与讨论了。 探究 [scode type="share"]在之后的不断讨论与探究中，主要从以下几个方面就行抉择：[/scode] [scode type="blue"]选择 springboot + redis 缓存[/scode] 保持与原先与eladmin相同的缓存方法，通过@CacheConfig与@Cacheable将缓存加在service中，仅仅只缓存以id或某字段为主键的记录（类似数据库中一条一条的数据），不缓存分页查询或自己封装后的数据。同时自己对哪些数据加了缓存，还要需要自己手动去维护。 @Service @AllArgsConstructor @CacheConfig(cacheNames = "role") public class RoleServiceImpl implements IRoleService { private final RoleMapper roleMapper; @Override @Cacheable(key = "'id:' + #p0") public Role getOneById(Serializable id) { return this.roleMapper.selectById(id); } @Override public List<Role> getRolesById(Serializable id) { return roleMapper.getRoleByUser(id); } } 这样加缓存的好处是缓存键值清晰，便于存取和删除，还可以建立缓存的命名空间，使缓存结构清晰易于管理。 [scode type="blue"]springboot缓存方式与@One、@Many之间的矛盾[/scode] 上述的缓存方式虽然很理想，但是会导致@One与@Many的查询无法正常走缓存，首先是@One与@Many方法只能调用mapper层的查询方法(调用service层会报错)，这就必须要求@CacheConfig与@Cacheable需要加在…