Redis分布式锁的实现(Jedis和Redisson两个方案)

应用场景

    分布式锁主要用于解决,公司中不一样业务系统对同一功能的数据产生脏读或重复插入。html

    好比公司现有三个小组分别开发WAP站、小程序、APP客户端,而这三个系统都存在领红包功能。java

    业务要求每人每日只能领取一个红包,若是有人同时登录三个系统那么就可以同一时间领取到三个红包。git

分布式锁的要求

分布式锁要知足如下基本要求:github

  1. 共享锁。多系统可以共享同一个锁机制。
  2. 互斥性。在任意时刻,只有一个请求能持有锁。
  3. 无死锁。在程序崩溃时可以,自动释放锁。
  4. 持有者解锁。锁只能被加锁的请求解锁,其余请求没法解锁。

Jedis实现分布式锁

本例参考了博文:https://wudashan.cn/2017/10/23/Redis-Distributed-Lock-Implement/redis

例子已上传码云:https://gitee.com/imlichao/jedis-distributed-lock-examplespring

添加依赖

本例使用spring boot提供的redis实现,并无直接引入jedis依赖。这样作的好处是,能够在项目中同时使用Jedis和RedisTemplate实例。数据库

pom.xml文件小程序

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

配置文件

application.properties文件缓存

#redis
spring.redis.database=0
spring.redis.host=18.6.8.22
spring.redis.password=Mfqy_redis_password_
spring.redis.port=6379
#链接超时时间(项目或链接池连接redis超时的时间)
spring.redis.timeout=2000
#最大链接数(建议为 业务指望QPS/单个链接的QPS,50000/1000=50)
spring.redis.pool.max-active=50
#最大空闲连接数(为减少伸缩产生的性能消耗,建议和最大链接数设成一致的)
spring.redis.pool.max-idle=50
#最小空闲链接数(0表明在无请求的情况下从不建立连接)
spring.redis.pool.min-idle=0
#链接池占满后没法获取链接时的阻塞时间(超时后抛出异常)
spring.redis.pool.max-wait=3000

Jedis工厂类

因为咱们使用了spring boot提供的redis实现,因此咱们不能直接获取到jedis对象。Jedis工厂类从RedisConnectionFactory中获取Redis链接(JedisConnection实现类),而后使用反射的方法从中取得了Jedis实例。服务器

/**
 * Jedis工厂类(单例模式)
 */
@Service
public class JedisFactory {
    @Autowired
    private RedisConnectionFactory connectionFactory;

    private JedisFactory(){}

    private static Jedis jedis;
    /**
     *  得到jedis对象
     */
    public Jedis getJedis() {
        //从RedisConnectionFactory中获取Redis链接(JedisConnection实现类),而后使用反射的方法从中取得了Jedis实例
        if(jedis == null){
            Field jedisField = ReflectionUtils.findField(JedisConnection.class, "jedis");
            ReflectionUtils.makeAccessible(jedisField);
            jedis = (Jedis) ReflectionUtils.getField(jedisField, connectionFactory.getConnection());
        }
        return jedis;
    }
}

为避免死锁的发生,加锁和设定失效时间必须是一个原子性操做。不然一旦在加锁后程序出错,没可以执行设置失效时间的方法时,就会产生死锁。 可是RedisTemplate屏蔽了插入数据和设置失效时间同时执行的方法,咱们只能获取到Jedis实例来执行。

分布式锁实现类

分布式锁主要实现了两个方法即占用锁和释放锁。

这里须要注意占用锁和释放锁都要保证原子性操做,避免程序异常时产生死锁。

锁id主要用于标识持有锁的请求,在释放琐时用来判断只有持有正确锁id的请求才能执行解锁操做。

/**
 * redis分布式锁
 */
@Service
public class DistributedLock {

    @Autowired
    private JedisFactory JedisFactory;

    /**
     * 占用锁
     * @param lockKey 锁key
     * @return 锁id
     */
    public String occupyDistributedLock(String lockKey) {
        //得到jedis实例
        Jedis jedis = JedisFactory.getJedis();
        //锁id(必须拥有此id才能释放锁)
        String lockId = UUID.randomUUID().toString();
        //占用锁同时设置失效时间
        String isSuccees = jedis.set(lockKey, lockId, "NX","PX", 15000);
        //占用锁成功返回锁id,不然返回null
        if("OK".equals(isSuccees)){
            return lockId;
        }else{
            return null;
        }
    }

    /**
     * 释放锁
     * @param lockKey 锁key
     * @param lockId 加锁id
     */
    public void releaseDistributedLock(String lockKey,String lockId) {
        if(lockId != null){
            //得到jedis实例
            Jedis jedis = JedisFactory.getJedis();
            //执行Lua代码删除lockId匹配的锁
            String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
            jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(lockId));
        }
    }
}

解释一下jedis.set(lockKey, lockId, "NX","PX", 15000)方法

格式 - String set(String key, String value, String nxxx, String expx, long time);
功能 - 存储数据到缓存中,并制定过时时间和当Key存在时是否覆盖。
参数 - 
key :redis key
value : redis值
nxxx: 只能取NX或者XX,若是取NX,则只有当key不存在是才进行set,若是取XX,则只有当key已经存在时才进行set
expx: 只能取EX或者PX,表明数据过时时间的单位,EX表明秒,PX表明毫秒。
time: 过时时间,单位是expx所表明的单位。

测试代码

Controller

/**
 * 分布式锁测试类
 */
@Controller
public class DistributedLockController {
    @Autowired
    private DistributedLock distributedLock;

    @RequestMapping(value = "/", method = RequestMethod.GET)
    public String index(){
        return "/index";
    }

    @RequestMapping(value = "/occupyDistributedLock", method = RequestMethod.GET)
    public String occupyDistributedLock(RedirectAttributes redirectAttributes, HttpServletRequest request){
        String key = "userid:55689";

        String lockId = null;
        try{
            //占用锁
            lockId = distributedLock.occupyDistributedLock(key);
            if(lockId != null){
                //程序执行
                TimeUnit.SECONDS.sleep(10);
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            //释放锁
            distributedLock.releaseDistributedLock(key,lockId);
        }
        redirectAttributes.addFlashAttribute("lockId",lockId);

        return "redirect:/";
    }
}

页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>distributed lock</title>
</head>
<body>
<h1>分布式锁测试</h1>
<button onclick="window.location.href = '/occupyDistributedLock'">占用锁</button>
<br/>
<!-- 占用成功返回锁id -->
<#if lockId??>${lockId}</#if>
</body>
</html>

Redisson实现分布式锁(推荐)

使用Redisson提供的分布式锁更加方便,并且锁的具体细节也不须要考虑。

例子已上传码云:https://gitee.com/imlichao/redisson-distributed-lock-example

官网:https://redisson.org/

文档:https://github.com/redisson/redisson/wiki

SpringBoot配置

添加依赖

spring boot 中引用专用依赖,会自动生成配置和spring bean的实例。

pom.xml文件

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.10.1</version>
</dependency>

配置文件

application.properties文件

#redisson
spring.redis.database=0
spring.redis.host=13.6.8.1
spring.redis.password=Mfqy_redis
spring.redis.port=6379

测试代码

Controller

/**
 * 分布式锁测试类
 */
@Controller
public class DistributedLockController {
    @Autowired
    private RedissonClient redisson ;

    @RequestMapping(value = "/", method = RequestMethod.GET)
    public String index(){
        return "/index";
    }

    @RequestMapping(value = "/occupyDistributedLock", method = RequestMethod.GET)
    public String occupyDistributedLock(RedirectAttributes redirectAttributes){
        RLock lock = null;
        try{
            //锁的key
            String key = "MF:DISTRIBUTEDLOCK:S:personId_1001";
            //得到分布式锁实例
            lock = redisson.getLock(key);
            //加锁而且设置自动失效时间15秒
            lock.lock(15, TimeUnit.SECONDS);
            //程序执行
            TimeUnit.SECONDS.sleep(10);
            //获取网络时间(多服务器测试统一时间)
            URL url=new URL("http://www.baidu.com");
            URLConnection conn=url.openConnection();
            conn.connect();
            long dateL=conn.getDate();
            Date date=new Date(dateL);
            //打印和返回结果
            System.out.println(date);
            redirectAttributes.addFlashAttribute("success",date);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            //释放锁
            if (lock != null) lock.unlock();
        }

        return "redirect:/";
    }
}

页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>distributed lock</title>
</head>
<body>
<h1>分布式锁测试</h1>
<button onclick="window.location.href = '/occupyDistributedLock'">占用锁</button>
<br/>
<#if success??>${success?datetime}</#if>
</body>
</html>

手动配置

添加依赖

pom.xml文件

<dependency>
     <groupId>org.redisson</groupId>
     <artifactId>redisson</artifactId>
     <version>3.10.1</version>
</dependency>

配置文件

application.properties文件

#redisson
spring.redis.database=0
spring.redis.host=13.6.8.1
spring.redis.password=Mfqy_redis
spring.redis.port=6379

配置类

/**
 * Redisson配置
 */
@Configuration
public class RedissonConfig {

    @Value("${spring.redis.database}")
    private int database;

    @Value("${spring.redis.host}")
    private String host;

    @Value("${spring.redis.password}")
    private String password;

    @Value("${spring.redis.port}")
    private String port;

    @Bean
    RedissonClient createConfig() {
        Config config = new Config();
        //设置编码方式为 Jackson JSON 编码(不设置默认也是这个)
        config.setCodec(new JsonJacksonCodec());
        //云托管模式设置(咱们公司用的阿里云redis产品)
        config.useReplicatedServers()
                //节点地址设置
                .addNodeAddress("redis://"+host+":"+port)
                //密码
                .setPassword(password)
                //数据库编号(默认0)
                .setDatabase(database);

        RedissonClient redisson = Redisson.create(config);
        return redisson;
    }
}

测试代码与SpringBoot配置同样

相关文章
相关标签/搜索