Ethon

为什么要有方法,因为懒惰是一种美德。

   :: 首页  :: 新随笔  ::  ::  :: 管理

分布式锁的原理:基于redis的setnx命令,setnx的作用就是设置一个key值,如果在redis中该key值不存在就设置成功,如果存在就会设置失败。在分布式集群环境下,多个服务器的线程同时设置一个key,哪个服务器的线程设置成功,就表示该服务器的线程获得了锁对象,其他线程必须等待。获得锁的线程需要记得,在某个时刻进行锁的释放(删除那个key)。

实现思路:

(1)获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。
(2)获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。
(3)释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.util.UUID;

/**
 * 分布式锁的工具类
 */
@Component
public class RedisLockUtil {

    @Autowired
    private RedisTemplate redisTemplate;

    //redis原始连接对象
    private RedisConnection redisConnection;

    //lua脚本的缓存签名字符串
    private String lockSha;
    private String unlockSha;

    private ThreadLocal<String> threadLocal = new ThreadLocal<>();

    /**
     * 添加分布式锁的Lua脚本(Lua脚本可以保证命令的原子性操作,因此在需要线程安全的操作时,我们可以考虑Lua脚本)
     */
    private String lockLua = "local key = KEYS[1]\n" +
            "local value = ARGV[1]\n" +
            "local time = ARGV[2]\n" +
            "\n" +
            "local result = redis.call('setnx', key, value)\n" +
            "if result == 1 then\n" +
            "  --当前获得了分布式锁\n" +
            "  --设置锁的过期时间\n" +
            "  redis.call('expire', key, time)\t\n" +
            "  return true\t\n" +
            "end\n" +
            "\n" +
            "--没有获得分布式锁\n" +
            "return false";

    //解锁的lua脚本
    private String unlockLua = "--要删除的是什么锁\n" +
            "local key = KEYS[1]\n" +
            "local uuid = ARGV[1]\n" +
            "\n" +
            "--获取锁中的uuid\n" +
            "local lockUUID = redis.call('get', key)\n" +
            "\n" +
            "--判断是不是自己上的锁\n" +
            "if uuid == lockUUID then\n" +
            "  --是自己上的锁,删除\n" +
            "  redis.call('del', key)\n" +
            "  return true\n" +
            "end\n" +
            "\n" +
            "--不是自己上的锁\n" +
            "return false";

    @PostConstruct
    public void init(){
        //获得原始连接
        redisConnection = redisTemplate.getConnectionFactory().getConnection();

        //缓存lua脚本
        lockSha = redisConnection.scriptLoad(lockLua.getBytes());
        unlockSha = redisConnection.scriptLoad(unlockLua.getBytes());
    }

    /**
     * 加锁的方法
     * @return
     */
    public boolean lock(String key, int timeout){

        String uuid = UUID.randomUUID().toString();
        //线程间的uuid数据隔离
        threadLocal.set(uuid);

        //执行加锁的lua脚本
        boolean flag = redisConnection.evalSha(lockSha, ReturnType.BOOLEAN, 1,
                key.getBytes(), uuid.getBytes(), (timeout + "").getBytes());

        return flag;
    }

    /**
     * 解锁的方法
     * @return
     */
    public boolean unlock(String key){
        //解锁原则:谁加的锁,谁来解锁。
        String uuid = threadLocal.get();

        //执行解锁的lua
        boolean flag = redisConnection.evalSha(unlockSha, ReturnType.BOOLEAN, 1,
                key.getBytes(), uuid.getBytes());

        return flag;
    }

}

 

分布锁的应用(Service层加锁)

import com.qf.stu.student_demo.dao.IStuDao;
import com.qf.stu.student_demo.entity.Student;
import com.qf.stu.student_demo.service.IStuService;
import com.qf.stu.student_demo.until.RedisLockUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.concurrent.TimeUnit;


@Service
public class StuServiceImpl implements IStuService {

    @Autowired
    private IStuDao stuDao;

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private RedisLockUtil redisLockUtil;
    /**
     * 分布式集群架构-分布式锁
     */
    @Override
    public List<Student> queryAll(){
        //先查询redis是否有缓存该数据
        List<Student> stus = (List<Student>) redisTemplate.opsForValue().get("stus");
        //判断缓存中是否存在
        if (stus == null) {
            //通过lua脚本获得分布式锁
            boolean flag = redisLockUtil.lock("lock", 120);
            if (flag){
                //获得分布式锁,进行缓存重建
                stus = stuDao.queryAll();//查询数据库
                //重建缓存
                redisTemplate.opsForValue().set("stus", stus);
                //设置过期时间
                redisTemplate.expire("stus", 5, TimeUnit.MINUTES);
                //释放锁
                redisLockUtil.unlock("lock");
            }else { //未拿到分布式锁,则休眠50毫秒后,再递归调用 queryAll()
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                return this.queryAll();
            }
        }
        return stus;
    }

/*    *//**
     * 单体架构
     * @return
     *//*
    @Override
    public List<Student> queryAll() {
        //先查询缓存
        List<Student> stus = (List<Student>) redisTemplate.opsForValue().get("stus");
        //判断缓存中是否存在
        if(stus == null){
            synchronized (this){
                stus = (List<Student>) redisTemplate.opsForValue().get("stus");
                if (stus == null){
                    System.out.println("查询了数据库");
                    stus = stuDao.queryAll();
                }
            }
            //重建缓存
            redisTemplate.opsForValue().set("stus",stus);
            //设置过期时间
            redisTemplate.expire("stus",5, TimeUnit.MINUTES);
        }

        return stus;
    }*/
}

 

================  SpringBoot提供的操作缓存服务器的API ================

@Cacheable:标记了当前注解的方法,在执行这个方法前,会先去缓存服务中查询数据,如果有就不会调用目标方法,如果没有再调用目标方法,并且重建缓存 - 主要作用于查询方法
@CachePut:该注解作用和@Cacheable差不多,唯一的区别在于被@CachePut注解标记的方法,一定会被执行。被标记方法的返回值会添加到缓存服务中 - 主要作用于添加的方法
@CacheEvict:根据表达式,删除缓存服务器的某个注解 - 主要用于修改和删除的方法
@Caching:可以帮助开发者在同一个方法上标记多个相同的缓存注解

import com.qf.stu.student_demo.dao.IStuDao;
import com.qf.stu.student_demo.entity.Student;
import com.qf.stu.student_demo.service.IStuService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
@Primary
public class StuServiceSpringCacheImpl implements IStuService {

    @Autowired
    private IStuDao stuDao;

    @Override
    @Cacheable(cacheNames = "stu",key = "'stulist'")
    public List<Student> queryAll() {
        System.out.println("查询所有学生的方法");
        return stuDao.queryAll();
    }

    @Override
    @CachePut(cacheNames = "stu",key = "'stuone' + #result.id")//从当前方法的返回值中找到id的属性,作为stuone的key值
    @CacheEvict(cacheNames = "stu",key ="'stulist'")
    public Student insert(Student student) {
        System.out.println("添加学生到数据库");
        stuDao.insert(student);
        return student;
    }

    @Override
    @Cacheable(cacheNames = "stu",key = "'stuone' + #id")//从参数中获取id
    public Student queryOne(Integer id) {
        System.out.println("根据id查询学生信息");
        return stuDao.queryOne(id);
    }

    @Override
    @Caching(evict = {
            @CacheEvict(cacheNames = "stu" ,key = "'stulist'"),
            @CacheEvict(cacheNames = "stu" ,key = "'stulist' + #id")
    })
    public int deleteById(Integer id) {
        return stuDao.deleteById(id);
    }
}

注意:使用这些SpringBoot提供的操作缓存服务器的API,启动类上必须加@EnableCaching注解

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

@SpringBootApplication
@EnableCaching
public class StudentDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(StudentDemoApplication.class, args);
    }

}

 

application.yml配置

redis:
host: xx.xx.xx.xx
password: root
cache:
type: redis
redis:
time-to-live: 60000
posted on 2019-04-17 19:59  Ethon  阅读(415)  评论(0编辑  收藏  举报