RedisTemplate 常用API+事务+陷阱+序列化+pipeline+LUA

3 篇文章 0 订阅

https://www.jianshu.com/p/7bf5dc61ca06/

https://blog.csdn.net/qq_34021712/article/details/79606551

https://www.jianshu.com/p/c9f5718e58f0

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

application.properties

简易测试:项目设置pool.max-active=10 ,客户端+redis服务器(1C1G)都是配置很低的PC,无其他业务代码,查询内容在20个字符以内。

用核心线程1000的线程池并发执行3000千次(Hash+String)查询,用时在1s左右。

用核心线程100的线程池并发执行3000千次(Hash+String)查询,用时在1100ms左右。

把设置改为pool.max-active=50,速度没有提升?可能是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.pool.max-active=8  
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.pool.max-wait=-1  
# 连接池中的最大空闲连接
spring.redis.pool.max-idle=8  
# 连接池中的最小空闲连接
spring.redis.pool.min-idle=1  
# 连接超时时间(毫秒)
spring.redis.timeout=0  

#哨兵的配置列表  配置哨兵就不用配置单独host
# redis主节点别名
spring.redis.sentinel.master=mymaster  #mymaster是redis-sentinel配置文件中的主机别名
# 哨兵集群 (这里配置的是sentinel节点,不是redis节点)
spring.redis.sentinel.nodes=39.107.119.254:26379,39.107.119.254:26380,39.107.119.254:26381

默认使用jedis作为底层实现。也可以配置成lettuce。lettuce与jedis都是redis的API。但是lettuce类是线程安全的可以通过一个单例对象为多线程提供API服务。jedis不是线程安全的,一个线程对应一个jedis对象。

spring:
  redis:
    lettuce:
      pool:
        # 连接池最大连接数(使用负值表示没有限制) 默认 8
        max-active: 100
        # 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1
        max-wait: -1
        # 连接池中的最大空闲连接 默认 8
        max-idle: 8
        # 连接池中的最小空闲连接 默认 0
        min-idle: 0

序列化Object

https://blog.csdn.net/u013958151/article/details/80603365

不同序列化方式,存在着解析性能和存储大小的差异,若比较敏感请通过批量读写测试对比。

JDK序列化

默认使用jdk的序列化 RedisTemplate<Object, Object>,其在Redis服务器中是乱码不能解析,可读比较差,但是性能比json格式好。下图是JDK序列化后的KEY-VALUE在Redis服务器中的显示效果。

序列化:Key=String;Value=String (灵活且兼容各种value格式)

RedisTemplate的K-V序列化都设置为StringRedisSerializer。以string格式读/写key-value,此种序列化格式与redis-cil执行指令时序列化格式匹配。在redis控制台中通过指令设置/保存的数据,可以通过此RedisTemplate完成正确的读写。

若value是json格式,可取出后手动再对String转换。

//spring默认的RedisTemplate可以直接使用
@Autowired
StringRedisTemplate stringRedisTemplate;

序列化:Key=String;Value=Json

自定义序列化RedisTemplate<Object, Object>,redisTemplate的Value序列化格式一定要与redis中Value保存的序列化格式一致,才能在读数据时正确的反序列化,否则会抛出jackson序列化异常。

:只能读取同样序列化配置的RedisTemplate所保存的value,否则会抛出jackson序列化异常。

 /**
     * redisTemplate 序列化使用的jdkSerializeable, 存储二进制字节码, 所以自定义序列化类
     * @param redisConnectionFactory
     * @return
     */
    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);

        // 使用Jackson2JsonRedisSerialize 替换默认序列化
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);

        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        //下面这行配置不注释,lua返回json会抛异常。Unexpected token (START_OBJECT)
        //objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);

        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

        // 设置value的序列化规则和 key的序列化规则
        // value: 将Object转化为Json 保存在Redis中
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); //另外一种JSON格式new GenericJackson2JsonRedisSerializer()
        redisTemplate.setKeySerializer(new StringRedisSerializer());

        //设置HASH序列化
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);

        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

下边所有测试都是基于上面Key=String;Value=Json的配置

以上序列化配置在Redis中的存储效果,String类型自动解析为"",自定义类型会记录全类名。

注意设置数字时要这样执行redisTemplate.opsForValue().set("1",1) ;

不要这样执行redisTemplate.opsForValue().set("1","1");这样将设置为字符串

第二种设置的是字符串,不支持任何数字操作api 

String-Json  value序列化= new Jackson2JsonRedisSerializer(Pojo.class)

//序列化 new Jackson2JsonRedisSerializer(Pojo.class);
@Autowired
private RedisTemplate<String, Pojo> redisTemplate;

SET-JSON

HASH <String,Dhhm>    value序列化= new Jackson2JsonRedisSerializer(Dhhm.class)

//序列化 new Jackson2JsonRedisSerializer(Dhhm.class);
@Autowired
private RedisTemplate<String, Dhhm> redisTemplate;

(Dhhm) redisTemplate.opsForHash().get("dhhm", "947607449"); 
//子key保存到redis时序列化为String,这里也必须是String

以上面配置的RedisTemplate<Object, Object>+ new Jackson2JsonRedisSerializer(Object.class)配置为例子,取出json-value是Object类型(实际底层是LinkedHashMap),不能强转为目标类型,需要通过Jackson转换。

以Zset为例子,其他数据结构同理:与上边例子对比,泛型为<Object,Object>时,redis不会记录class信息。

jackson转换类型:com.fasterxml.jackson.core.type.TypeReference<Set<Pojo>>,支持泛型属性转换

redis中存储格式:

{
  "name": "id1",
  "map": {
    "1": "2",
    "2": [1,2,3,4]
  },
  "list": [
    {
      "name": "son",
      "map": {
        
      },
      "list": [
        {
          "name": "hhhhh",
          "map": {
            
          },
          "list": []
        }
      ]
    }
  ]
}
@Test
    public void testRedisZsetJson() throws IOException {
       ZSetOperations<Object, Object> zSetOperations = redisService.getRedisTemplate().opsForZSet();
        //写
        Pojo pojo = new Pojo();
        pojo.setName("id1");
        pojo.getMap().put("1","2");
        pojo.getMap().put("2", Arrays.asList(1,2,3,4));

        Pojo son = new Pojo();
        son.setName("son");
        //泛型List
        pojo.getList().add(son);

        son.getList().add( new Pojo("hhhhh"));

        zSetOperations.add("jsonZset",pojo,10);
        //重复提交pojo,set去重,以最后为准
        zSetOperations.add("jsonZset",pojo,11);
        
        //读
        ObjectMapper objectMapper = new ObjectMapper();
        Set<Object> jsonZset = zSetOperations.range("jsonZset", 0, 200);
        //因为使用的RedisTemplate<Object, Object>+json 所以直接转换Pojo类型有java.lang.ClassCastException: java.util.LinkedHashMap

        //jackson做类型转换和泛型支持
        Set<Pojo> pojos = objectMapper.convertValue(jsonZset, new TypeReference<Set<Pojo>>() {});

        for (Pojo o : pojos) {
            System.out.println(o);
            System.out.println(o.getList().get(0));
            System.out.println(o.getList().get(0).getList().get(0));
            //删
            //zSetOperations.remove("jsonZset",o);
        }
    }

class Pojo {

    private String name;

    private HashMap<Object,Object> map=new HashMap();

    private List<Pojo> list =new ArrayList<>();

    public Pojo() {
    }

    public Pojo(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public HashMap<Object, Object> getMap() {
        return map;
    }

    public void setMap(HashMap<Object, Object> map) {
        this.map = map;
    }

    public List<Pojo> getList() {
        return list;
    }

    public void setList(List<Pojo> list) {
        this.list = list;
    }

    @Override
    public String toString() {
        return "Pojo{" +
                "name='" + name + '\'' +
                ", map=" + map +
                '}';
    }
}

执行结果 

Pojo{name='id1', map={1=2, 2=[1, 2, 3, 4]}, list=[Pojo{name='son', map={}, list=[Pojo{name='hhhhh', map={}, list=[]}]}]}
Pojo{name='son', map={}, list=[Pojo{name='hhhhh', map={}, list=[]}]}
Pojo{name='hhhhh', map={}, list=[]}

常用API

以下以RedisTemplate<String, String>为例

Key

redisTemplate.

keys(String pattern):Set<String>
hasKey(String k):Boolean
delete(String k)
expire(String k,long timeout,TimeUnit unit)
expireAt(String k,Date date)
getExpire(String k)
getExpire(String k,TimeUnit unit)
watch(String k)
watch(Collection<String> keys)
multi()
exec():List<Object>
discard()

String

redisTemplate.opsForValue()

set(String k, String v) :void
get(Object k) :String 
getAndSet(String k, String v) :String 
setIfAbsent(String k, String v) :Boolean //SETNX
increment(String k, long v) :Long //加v 返回运算后结果
increment(String k, double v) :Double //加v 返回运算后结果

List

redisTemplate.opsForList()

size(String k)
range(String k, long s, long e) :List<String> //返回list k的从s到e元素
leftPush(String k, String v) :Long  //还有对应的right版
leftPushIfPresent(String k, String v):Long
leftPop(String k) :String  //没有会阻塞
leftPop(String  key, long time, TimeUnit unit):String
set(String k,long index,String value)
remove(String k,long count,Object value):Long //移除count个,value相等的值,返回移除个数

Hash

redisTemplate.opsForHash()

put(String key, Object hashKey,Object value)
putIfAbsent(String key,Object hashKey,Object value):Boolean
delete(String key,Object... hashKeys):Long	
get(String key, Object hashKey):Object
increment(String key,Object hashKey,long delta):Long  //还有Double版 返回计算结果
size(String key):Long	
hasKey(key, hashKey):Boolean
keys(key):Set<Object>
values(key):List<Object>
entries(key):Map<Object,Object>

RedisTemplate事务

watch、unwatch、multi、exec、discard 

可以实现CAS,非原子性事务批量执行,按顺序地串行化整体执行而不会被其它命令插入。multi命令使Redis将这些命令放入queue,而不是执行这些命令。当放入queue失败时(例如:语法错误),EXEC命令可以检测到并且不执行。一旦调用EXEC命令成功,那么Redis就会一次性执行事务中所有命令,中间某条指令的失败不会导致前面已做指令的回滚,也不会造成后续的指令不做。相反,调用DISCARD命令将会清除事务队列,然后退出事务。EXEC命令的返回值是一个数组,其中的每个元素都分别是事务中的每个命令的返回值,返回值的顺序和命令的发出顺序是相同的。

//CAS 伪代码
//注意MULTI中所有命令 返回NULL,包括GET
WATCH mykey
val = GET mykey
val = val + 1
MULTI
SET mykey $val
EXEC

RedisTemplate事务陷阱

https://blog.csdn.net/qq_34021712/article/details/79606551(Spring 事务陷阱,永不关闭连接)

https://www.jianshu.com/p/c9f5718e58f0

陷阱1:不关闭连接问题

如果你的redisTemplate已经开启了事务支持 redisTemplate.setEnableTransactionSupport(true),

且在未标明@Transactional的方法内使用时,需要手动释放链接,否则链接一直不解绑关闭,造成无可用连接问题,或内存爆满

redisTemplate.setEnableTransactionSupport(true);

public Object get(String key){
	Object o = redisTemplate.opsForValue().get(key);
        //解绑 释放链接
	TransactionSynchronizationManager.unbindResource(redisTemplate.getConnectionFactory());
	return o;
}

陷阱2:事务将命令放到Queue中,因为命令并没有执行,所以无法拿到返回值,代码片段如下:

假设当前在multi事务中

String i =redisTemplate.opsForValue().get("6666").toString;
Long increment = redisTemplate.opsForValue().increment("6666", 1);

System.out.println(i);//null
System.out.println(increment);//null

是无法拿到返回值的,是因为redis在MULTI/EXEC代码块中,命令都会被delay,放入Queue中,而不会直接返回对应的值。即例子中的increment自增,自增命令执行完成,默认是会返回自增之后的值,但是却是返回了null.

解决思路

前提必须设置redisTemplate.setEnableTransactionSupport(true)

方法1: @Transactional 来声明 Redis 事务的范围,中间发生异常全部不执行(因为Redis事务是放入队列,Exec时一起执行)

redisTemplate.setEnableTransactionSupport(true);

@Transactional(rollbackFor = Exception.class)
public String put() {
        int i = (int)(Math.random() * 100);
        template.opsForValue().set("key"+i, "value"+i, 300, TimeUnit.SECONDS);
        return "success "+"key"+i;
}
//方法中可以同时有Redis 和 SQL

我们可以先在 Spring 语境里配置一个 PlatformTransactionManager(例如 DataSourceTransactionManager),然后再用 @Transactional 注释来声明 Redis 事务的范围,让 Spring 自动关闭 Redis 连接。
另外,我们还发现了 Redis 事务和关系数据库事务(在本例中,即 JDBC)相结合的不利之处。混合型事务的表现和预想的不太一样。

方法2: 无需@Transactional,手动执行事务(比较灵活),中间发生异常全部不执行(因为Redis事务是放入队列,Exec时一起执行)

redisTemplate.setEnableTransactionSupport(true);

无@Transactional ,手动配置执行事务,txResults 顺序的各行命令的执行结果

List<Object> txResults = redisTemplate.execute(new SessionCallback<List<Object>>() {
  public List<Object> execute(RedisOperations operations)  {
    operations.multi();
    operations.opsForSet().add("key", "value1");
    return operations.exec();
  }
});

事务建议

  • 升级到springboot 2.0以上版本,如果因为项目原因无法升级看下面的建议
  • 如果使用Redis事务的场景不多,完全可以自己管理(方法2),不需要使用spring的注解式事务。
  • 如果一定要使用spring提供的注解式事务,建议初始化两个RedisTemplate Bean,分别设置enableTransactionSupport属性为true和false。针对需要事务和不需要事务的操作使用不同的template。
  • 从个人角度,我不建议使用redis事务,因为redis对于事务的支持并不是关系型数据库那样满足ACID。Redis事务只能保证ACID中的隔离性和一致性,无法保证原子性和持久性。而我们使用事务最重要的一个理由就是原子性,这一点无法保证,事务的意义就去掉一大半了。所以事务的场景可以尝试通过业务代码来实现。

 

陷阱

事务中的陷阱已经在上面说明,下面记录一些常用操作陷阱。

1 不释放链接问题

https://segmentfault.com/a/1190000021204378?utm_source=tag-newest

stringRedisTemplate 对redis常规操作做了一些封装,但还不支持像 Scan SetNx等命令,这时需要拿到jedis Connection进行一些特殊的Commands。使用 stringRedisTemplate.getConnectionFactory().getConnection() 是不被推荐的,因为这需要手动释放链接。如果没有代码中手动释放则会触发连接池中无可用连接,大量请求阻塞或失败。

推荐使用下面这种方式拿到RedisConnection。RedisTemplate源码中也是使用execute(RedisCallback)执行。

Set<Object> execute = redisTemplate.execute(new RedisCallback<Set<Object>>() {

    @Override
    public Set<Object> doInRedis(RedisConnection connection) throws DataAccessException {

        Set<Object> binaryKeys = new HashSet<>();
        //执行scan
        Cursor<byte[]> cursor = connection.scan( new ScanOptions.ScanOptionsBuilder().match("test*").count(1000).build());
        while (cursor.hasNext()) {
            binaryKeys.add(new String(cursor.next()));
        }
        //不需要手动关闭链接
        return binaryKeys;
    }
});


pipeline管道化

使用Pipeline合并请求减少TCP链接次数。客户端允许将多个请求一次发给服务器,过程中而不需要等待请求的回复,在最后再一并读取结果即可。

  • pipeline机制可以优化吞吐量,但无法提供原子性/事务保障,而这个可以通过Redis-Multi等命令实现。
  • 部分读写操作存在相关依赖,无法使用pipeline实现,可利用Script机制,但需要在可维护性方面做好取舍

https://www.cnblogs.com/littleatp/p/8419796.html

jedis操作
Pipeline pipeline = jedis.pipelined();
int j;
for (j = 0; j < batchSize; j++) {
     if (i + j < cmdCount) {
           pipeline.set(key(i + j), UUID.randomUUID().toString());
     } else {
           break;
     }
}
pipeline.sync(); //等待
 List<Long> List = redisTemplate.executePipelined(new RedisCallback<Long>() {
            @Nullable
            @Override
            public Long doInRedis(RedisConnection connection) throws DataAccessException {
                connection.openPipeline();
               for (int i = 0; i < 1000000; i++) {
                    String key = "123" + i;
                    connection.zCount(key.getBytes(), 0,Integer.MAX_VALUE);
                }
                return null;
            }
        });

 

令牌桶LUA实现

原文:https://my.oschina.net/lyyjason/blog/1608213

redission实现:https://github.com/redisson/redisson/wiki/6.-%E5%88%86%E5%B8%83%E5%BC%8F%E5%AF%B9%E8%B1%A1#612-%E9%99%90%E6%B5%81%E5%99%A8ratelimiter

将添加令牌改成触发式的方式,取令牌同时做添加令牌的动作(惰性处理,类似于TTL),也可以定时任务处理。

  • curr_mill_second = 当前毫秒数
  • last_mill_second = 上一次添加令牌的毫秒数
  • r = 添加令牌的速率
  • reserve_permits = (curr_mill_second-last_mill_second)/1000 * r  //通过时间差和添加速率,算出本次要添加数量
--- 获取令牌
--- 返回值
--- 0 没有令牌桶配置
--- -1 表示取令牌失败,也就是桶里没有令牌
--- 1 表示取令牌成功
--- 入参
--- @param key 令牌(资源)的唯一标识
--- @param permits 请求令牌数量
--- @param curr_mill_second 当前毫秒数
--- @param context 使用令牌的应用标识

local function acquire(key, permits, curr_mill_second, context)
local rate_limit_info = redis.pcall("HMGET", key, "last_mill_second", "curr_permits", "max_permits", "rate", "apps")
local last_mill_second = rate_limit_info[1]
local curr_permits = tonumber(rate_limit_info[2])
local max_permits = tonumber(rate_limit_info[3])
local rate = rate_limit_info[4]
local apps = rate_limit_info[5]

--- 标识没有配置令牌桶
if type(apps) == 'boolean' or apps == nil or not contains(apps, context) then
return 0
end
local local_curr_permits = max_permits;

--- 令牌桶刚刚创建,上一次获取令牌的毫秒数为空
--- 根据和上一次向桶里添加令牌的时间和当前时间差,触发式往桶里添加令牌,不超过桶的最大值
--- 并且更新上一次向桶里添加令牌的时间
--- 如果向桶里添加的令牌数不足一个,则不更新上一次向桶里添加令牌的时间
if (type(last_mill_second) ~= 'boolean' and last_mill_second ~= false and last_mill_second ~= nil) then
local reverse_permits = math.floor(((curr_mill_second - last_mill_second) / 1000) * rate)
local expect_curr_permits = reverse_permits + curr_permits;
local_curr_permits = math.min(expect_curr_permits, max_permits);

--- 大于0表示不是第一次获取令牌,也没有向桶里添加令牌
if (reverse_permits > 0) then
redis.pcall("HSET", key, "last_mill_second", curr_mill_second)
end
else
redis.pcall("HSET", key, "last_mill_second", curr_mill_second)
end


local result = -1
if (local_curr_permits - permits >= 0) then
result = 1
redis.pcall("HSET", key, "curr_permits", local_curr_permits - permits)
else
redis.pcall("HSET", key, "curr_permits", local_curr_permits)
end

return result
end

 

 

执行LUA

https://blog.csdn.net/u014495560/article/details/82531046

串行化执行保证:原子性、有序性、可见性、减少通讯次数、无回滚

Lua 脚本项目的 resources 目录下,起名 limit.lua 即可

注意:KEYS[1],ARGV[1]必须大写,数组从1开始。

local key = KEYS[1]; //必须大写
local dzyid=ARGV[1];

--转数字(对应key的值必须是redis数字才行,否则nil)
local current =tonumber( redis.call('get', key) ); 

--tonumber功能并不是将字符串转为数字,而是将redis数字转换LUA数字,为了在LUA中进行计算

if (current==1) then --数字比较
    return current+1;
end

源码中写到ResultType支持:Long, Boolean, List, or deserialized value type 。List代表lua返回数组。

   
    /**
     * 适用于lua返回数字。
     * @return 
     */
    @Bean
    public DefaultRedisScript<Number> redisluaScript() {
        DefaultRedisScript<Number> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("limit.lua")));
        redisScript.setResultType(Number.class); //返回类型
        return redisScript;
    }

    @Bean("simlockredisluaScriptLong")
    public DefaultRedisScript<Long> simlockredisluaScriptLong() {
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lockdhhm.lua")));
        //源码中写到ResultType支持:Long, Boolean, List, or deserialized value type
        redisScript.setResultType(Long.class);//返回值类型 不要用Integer
        return redisScript;
    }

    /**
     * 适用于lua返回string、json字符串。
     * @return String
     */
    @Bean
    public DefaultRedisScript<String> redisluaScriptStrig() {
        DefaultRedisScript<String> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("limit.lua")));
        redisScript.setResultType(String.class);//返回类型
        return redisScript;
    }

    /**
     * 适用于lua返回List、Set、lua-table。
     * 使用RedisTemplate<String, String>执行脚本,返回List<String>类型
     * @return List
     */
    @Bean("redisluaScriptList")
    public DefaultRedisScript<List> redisluaScriptList() {
        DefaultRedisScript<List> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/getset.lua")));
        redisScript.setResultType(List.class);
        return redisScript;
    }
    @Autowired
    StringRedisTemplate stringRedisTemplate;
 
    @Autowired
    private DefaultRedisScript<Number> redisluaScript;

    List<String> keys= Arrays.asList("key1");
    //执行LUA  <T> T execute(RedisScript<T> var1, List<K> var2, Object... var3);
    //args 传字符串类型,否则会报错。在lua中转数字tonumber()。
    Number number = redisTemplate.execute(redisluaScript,keys, arg1, arg2);

注意: 序列化格式与存储格式、脚本返回类型匹配、Value类型与LUA数字计算、入参类型,一定要与redis中实际存储的数据类型匹配,否则会有各种异常。

1 序列化:redisTemplate的序列化(String、Json、JDK序列化...)一定要与redis中Value保存的序列化格式一致,才能在get时正确的反序列化,否则会抛出异常。

2 返回值类型:T redisTemplate.execute返回值类型,取决于DefaultRedisScript设置的返回值类型。

3 lua的返回值类型: lua的返回值类型要与DefaultRedisScript设置的返回值类型完全匹配。比如value=1,返回值类型必须是Long,DefaultRedisScript设置为返回值String也会报错。所以可以设置返回值类型为Object,然后在通过instanceof判断类型。

4 redis存储格式与LUA:注意设置数字时要这样执行redisTemplate.opsForValue().set("1",2) 。

                                        不要这样执行redisTemplate.opsForValue().set("1","2");

第二种设置的是字符串 ,LUA不支持对其tonumber()和运算,tonumber并不是将字符串转为数字。

5 若LUA返回值为数字,要用Long/Number做JAVA的返回类型,不要用Integer;(会出现IllegalStateException

6 参数类型一定和redis中匹配。args 最好传字符串类型,否则可能会报错。在lua中转数字tonumber(AVGE[1])。

 

 

常见异常报错

例1

Caused by: io.lettuce.core.RedisCommandExecutionException: ERR Error running script (call to f_47e4c946b9a30705a03eded83a2e086b54791c44): @user_script:3: user_script:3: attempt to perform arithmetic on local 'current' (a nil value) 

解释 在lua脚本的第3行出错,local变量current是nil

例2

SerializationException: Could not read JSON 

序列化异常,要保证返回值格式符合redisTemplate解析格式。设置redisTemplate解析value为JSON,若LUA直接返回字符串需要加“”,否则无法解析JSON。

if (haskey == 0) then
    return '"失败"';
else
    return '"成功"';
end

例3

java.lang.IllegalStateException

LUA返回值为整数时出现此异常,确定请在JAVA配置的返回值类型为Long而不是Integer。

例4

集群环境:EvalSha is not supported in cluster environment

需要切换使用LettuceConnectionFactory

@Bean
LettuceConnectionFactory redisConnectionFactory(RedisClusterConfiguration configuration) {
    return new LettuceConnectionFactory(configuration);
}

例5

集群环境:CROSSSLOT Keys in request don't hash to the same slot--keys不能落在同一个节点

keys.add("{test}key1");
keys.add("{test}key2");

LUA预加载

启动程序时可以将lua脚本预加载到redis中返回sha1值,执行simlockredisluaScriptLong脚本时只需要发送sha1+参数到redis。

减少了网络开销。

    @PostConstruct
    public void loadScript() {
        //RedisCallback自动释放connection
        String execute = redisTemplate.execute((RedisCallback<String>) connection -> {
            return connection.scriptLoad(simlockredisluaScriptLong.getScriptAsString().getBytes());
        });  
    }

 

LUA语法

注意:KEYS[1],ARGV[1]大写,且数组从1开始计数

1 数字相关的比较/计算必须要用tonumber()转化

if (tonumber(var)==tonumber(var1)) then

end

2 解析JSON  cjson.decode。

例子:https://blog.csdn.net/fsw4848438/article/details/81540495

 redis中的lua已经安装了 lua-cjson 库。不需要手动安装。

注意:在配置redisTemplate的jackson序列化时不要设置objectMapper.enableDefaultTyping();

这个配置使Jackson把数组的json字符串反序列化为List时候报Unexpected token (START_OBJECT)

--json字符串
local sampleJson = [[{"age":"23","testArray":{"array":[8,9,11,14,25]},"Himi":"himigame.com"}]];
--解析字符串,转化为json对象
local data = cjson.decode(sampleJson);
--打印json字符串中的age字段
print(data["age"]);
--打印数组中的第一个值(lua默认是从0开始计数)
print(data["testArray"]["array"][1]);

3 创建JSON  cjson.encode

lua返回json时,实际返回的是list<map>,map为json本体。

-- lua table
local result ={}
--可以不传类型,以标识/业务约定。由java代码自主判断json的解析类型
--result["@class"] = "com.common.Result"
result["success"] = true
result["data"] = redis.call("get",key1)

--转化为json字符串
return cjson.encode(result)

4 字符串拼接..

'str1'..'str2'
'str1','str2'

5 判断redis.call('get',limitkey )返回null

 redis没有get到key返回null,此时LUA中返回的结果不是 nil而是 userdata类型的 ngx.null。

 当使用lua脚本执行逻辑时,如果要判断这个值,很容易让人迷惑以为它是nil,从而导致判断不成立,实际它是一个boolean的值

在lua中,除了nil和false,其他的值都为真,包括0,可以通过nil为false这一点来判断是否为空

local current = redis.call('get', key);
--在lua中,除了nil和false,其他的值都为真,包括0,可以通过nil为false这一点来判断是否为空
if current then
    return '"存在"';
else
    return '"不存在"';
end

6 类型判断

type(apps) == 'boolean' //类型是布尔

 7 字符串包含

if not contains(apps, context) then //apps中包含context

一个简单LUA案例

模拟多人并发抢号的场景,抢到号码后锁定10分钟。10分钟后不操作释放号码。

redis中预热保存了50个号码测试信息,key为号码,value为号码信息。

 

--加载lua脚本    
@Bean("simlockredisluaScriptLong")
    public DefaultRedisScript<Long> simlockredisluaScriptLong() {
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lockdhhm.lua")));
        redisScript.setResultType(Long.class);//返回值类型 不要用Integer
        return redisScript;
    }
@Autowired
@Qualifier("simlockredisluaScriptLong")
DefaultRedisScript<Long> simlockredisluaScriptLong;

int dhhm=111125730; //目标号码
String code="人员ID1";

--执行 注意参数的类型与redis匹配
Long result = redisTemplate.execute(simlockredisluaScriptLong, Arrays.asList(), dhhm, code);

LUA脚本代码 lockdhhm.lua

验证号码合法,验证个人锁定次数防刷,尝试加锁10分钟

--返回负值=失败
local key = 'sim:dhhm'; --号码列表
local dhhm = ARGV[1]; --目标号码
local dzyid = ARGV[2]; --员工ID
--local fdbs=ARGV[3]; --分店
local lockkey = 'sim:lock:' .. dhhm;

--检验号码已被锁(加速返回,不做后边的操作)
local belock = redis.call('get', lockkey);
if belock then
    --在lua中,除了nil和false,其他的值都为真,包括0,可以通过nil为false这一点来判断是否为空
    --已存在
    return -3;
end

--检验号码存在
if (redis.call('hexists', key, dhhm) == 0) then
    return -1;
end

--防刷 每个员工ID每个号码一小时只能锁3次
local limitkey = 'sim:limit:' .. dhhm .. ':' .. dzyid;

local limitnum = redis.call('get', limitkey);
if limitnum then
    if (tonumber(limitnum) > 2) then
        return -2;
    end
end

--尝试号码加锁 10分钟
local snkey = 'sim:sn:' .. dhhm;
if (redis.call('setnx', lockkey, dzyid) > 0) then
    redis.call('expire', lockkey, 600);

    --防刷 记录员工ID锁定此号次数
    if (redis.call('setnx', limitkey, 1) > 0) then
        --首次
        redis.call('expire', limitkey, 3600);
    else
        --累计
        redis.call('incr', limitkey);
    end

    --此序号用于携带给消费者,消费者核对当前消费是不是此号码最新加锁序号
    return redis.call('incr', snkey);
else
    return -3;
end


 测试模拟多人并发抢同一号码

 --模拟多人并发抢同一号码

    @Autowired
    DefaultRedisScript<Long> simlockredisluaScriptLong;

    @Test
    public void simlock() throws InterruptedException {
        int num=100;
        int dhhm=111125730; //抢夺目标号码
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(50, 50,
                3, TimeUnit.MINUTES, new ArrayBlockingQueue<>(num));
        CountDownLatch begin = new CountDownLatch(1);
        CountDownLatch end = new CountDownLatch(num);

        for (int i = 0; i < num; i++) {
            threadPoolExecutor.execute(() -> {
                try {
                    //测试 随机生成人员编号
                    String dzyid=String.valueOf(new Random().nextInt(100));
                    System.out.println(dzyid+"准备就绪");
                    begin.await();

                    Long result  = redisTemplate.execute(simlockredisluaScriptLong, Arrays.asList(), dhhm, dzyid);
                    if (result >=0){
                        System.out.println(dzyid+"成功锁定"+dhhm);
                    }
                    end.countDown();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

        System.out.println("begin:"+System.currentTimeMillis());
        begin.countDown();
        end.await();
        System.out.println("end:"+System.currentTimeMillis());
    }

运行后显示 "84成功锁定111125730",检查Redis数据库正确生成对应的锁

Spring boot实现监听Redis key失效事件实现

原文:https://blog.csdn.net/zwrlj527/article/details/88876315

修改: redis.conf 开启相关时间推送。

  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值